๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Project

[ํ”„๋กœ์ ํŠธ ๊ธฐ๋ณธ ์„ธํŒ…] Account ๊ด€๋ จ DTO, Controller, Service ์ƒ์„ฑ ํ›„ Test

ํ”„๋กœ์ ํŠธ ๊ธฐ๋ณธ ์„ธํŒ…

4.  [๋ฐฑ์—”๋“œ] Account ๊ด€๋ จ DTO, Controller, Service ์ƒ์„ฑ ํ›„ Test


๊ณ„์ • ๊ด€๋ จ DTO ์ƒ์„ฑ

ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ํšŒ์› ์ •๋ณด ์ „๋‹ฌ, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ๊ณผ ๊ด€๋ จ๋œ DTO ๋“ค์„ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด์ค€๋‹ค.

ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ž…๋ ฅ๋ฐ›๋Š” ๊ฐ’์€ validation ์กฐ๊ฑด์„ ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๊ฑธ์–ด์ค€๋‹ค.

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฐ ์—ฌ๋Ÿฌ ๋‹ค๋ฅธ ์ƒํ™ฉ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด @Builder๋„ ์„ ์–ธํ•ด ์ฃผ์—ˆ๋‹ค.

SignUpRequest.java

package com.portfolio.backend.account.dto;

// ... import ์ƒ๋žต

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SignUpRequest {

    @NotBlank
    @Email
    private String email;

    @NotBlank
    @Length(min=3, max = 20)
    @Pattern(regexp = "^[ใ„ฑ-ใ…Ž๊ฐ€-ํžฃa-zA-Z0-9_-]{3,20}$")
    private String nickname;

    @NotBlank
    @Length(min=6, max = 50)
    private String password;
}

ํšŒ์› ๊ฐ€์ž… Request DTO ๊ฐ์ฒด์ด๋‹ค.

@Pattern(regexp = "^[ใ„ฑ-ใ…Ž๊ฐ€-ํžฃa-zA-Z0-9_-]{3,20}$")

์ •๊ทœ ํ‘œํ˜„์‹์œผ๋กœ ๋‹ค๋ฅธ ํŠน์ˆ˜๋ฌธ์ž๊ฐ€ ๋“ค์–ด์˜ฌ ์ˆ˜ ์—†๊ฒŒ Validation ํ•ด์ค€๋‹ค.

SignInRequest.java

package com.portfolio.backend.account.dto;

// ... import ์ƒ๋žต

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SignInRequest {

    @NotBlank
    @Email
    private String email;

    @NotBlank
    @Length(min=6, max = 50)
    private String password;
}

ํšŒ์› ๊ฐ€์ž… Request DTO ๊ฐ์ฒด์ด๋‹ค.

AccountInfoResponse.java

package com.portfolio.backend.account.dto;

// ... import ์ƒ๋žต

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AccountInfoResponse {
    private Long id;
    private String email;
    private String nickname;
    private boolean isCertify;
    private List<String> roles;
    private LocalDateTime createdDate;
    private LocalDateTime modifiedDate;

    public AccountInfoResponse(Account account){
        this.id = account.getId();
        this.email = account.getEmail();
        this.nickname = account.getNickname();
        this.isCertify = account.isCertify();
        this.roles = account.getRoles();
        this.createdDate = account.getCreatedDate();
        this.modifiedDate = account.getModifiedDate();
    }
}

ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ •ํ™•ํ•œ JWT ํ† ํฐ์„ ๋ณด๋‚ด์คฌ์„ ๋•Œ ๋ฐฑ์—”๋“œ์—์„œ ๋ณด๋‚ด์ฃผ๋Š” ํšŒ์›์ •๋ณด Response DTO ๊ฐ์ฒด์ด๋‹ค.

PasswordUpdateRequest.java

package com.portfolio.backend.account.dto;

// ... import ์ƒ๋žต

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PasswordUpdateRequest {

    @NotBlank
    @Length(min=6, max = 50)
    private String beforePassword;

    @NotBlank
    @Length(min=6, max = 50)
    private String updatePassword;

}

๋น„๋ฐ€๋ฒˆํ˜ธ ๊ต์ฒด API๋ฅผ ์œ„ํ•œ Request DTO์ด๋‹ค.

๊ธฐ์กด์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ์ˆ˜์ •ํ•˜๊ณ  ์‹ถ์€ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋‘ ๊ฐ€์ง€๋ฅผ ๋ฐ›๋Š”๋‹ค.

Service ๋‹จ์—์„œ ๊ธฐ์กด์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•˜๊ณ  ๋งž์œผ๋ฉด ๊ต์ฒดํ•ด ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๋งŒ๋“ค ์˜ˆ์ •์ด๋‹ค.

feat: ๊ณ„์ • ๊ด€๋ จ Controller, Service ์ƒ์„ฑ ๋ฐ ํ•„์š”ํ•œ Exception, Repository ์ˆ˜์ •

์œ„์—์„œ ์ž‘์„ฑํ•œ DTO๋“ค์„ ์ด์šฉํ•ด ๊ณ„์ •์— ๊ด€๋ จ๋œ ์ปจํŠธ๋กค๋Ÿฌ์™€ ์„œ๋น„์Šค ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ณ  ํ•„์š”ํ•œ Exception๊ณผ Repository ์ˆ˜์ •๋„ ํ•จ๊ป˜ํ•ด ์ค€๋‹ค.

AccountController.java

package com.portfolio.backend.account;

// ... import ์ƒ๋žต

@Api(tags = {"Account API"})
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/api")
public class AccountController {

    private final  AccountService accountService;

    @ApiOperation(value = "ํšŒ์› ๊ฐ€์ž… API", notes = "์ด๋ฉ”์ผ, ๋‹‰๋„ค์ž„, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ „์†ก")
    @PostMapping(value = "/sign-up")
    public CommonResponse signUp(@ApiParam(value = "ํšŒ์›๊ฐ€์ž… ์š”์ฒญ ๊ฐ์ฒด", required = true) @RequestBody @Valid SignUpRequest dto, Errors errors){
        if(errors.hasErrors()) throw new CustomValidationException();
        return accountService.signUp(dto);
    }

    @ApiOperation(value = "๋กœ๊ทธ์ธ API",notes = "ํ•™๋ฒˆ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ „์†ก")
    @PostMapping(value =  "/sign-in")
    public SingleResponse<String> signIn(@ApiParam(value = "๋กœ๊ทธ์ธ ์š”์ฒญ ๊ฐ์ฒด",required = true) @RequestBody @Valid SignInRequest dto, Errors errors){
        if(errors.hasErrors()) throw new CustomValidationException();
        return accountService.signIn(dto);
    }

    @ApiImplicitParams({
            @ApiImplicitParam(name = "X-AUTH-TOKEN", value = "๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ํ† ํฐ", required = false, dataType = "String", paramType = "header")
    })
    @ApiOperation(value = "๋กœ๊ทธ์ธํ•œ ํšŒ์› ์กฐํšŒ", notes = "๋กœ๊ทธ์ธ ํ›„ ๋ฐ›์€ ํ† ํฐ์œผ๋กœ ์ธ์ฆํ•œ๋‹ค.")
    @GetMapping(value = "/account")
    public SingleResponse<AccountInfoResponse> getAccountInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String email = authentication.getName();
        return accountService.getAccountInfo(email);
    }

    @ApiImplicitParams({
            @ApiImplicitParam(name = "X-AUTH-TOKEN", value = "๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ํ† ํฐ", required = false, dataType = "String", paramType = "header")
    })
    @ApiOperation(value = "๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ API", notes ="์ด์ „ ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ƒˆ๋กœ์šด ๋น„๋ฐ€๋ฒˆํ˜ธ ์ „์†ก")
    @PutMapping(value = "/account/password")
    public SingleResponse<String> updatePassword(@ApiParam(value = "๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ๊ฐ์ฒด", required = true) @RequestBody @Valid PasswordUpdateRequest dto, Errors errors){
        if(errors.hasErrors()) throw new CustomValidationException();
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String email = authentication.getName();
        return accountService.updatePassword(dto, email);
    }
}

Swagger์—์„œ์˜ API ๋ฌธ์„œํ™” ๋ฐ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๊ด€๋ จ ์–ด๋…ธํ…Œ์ด์…˜๋“ค๋„ ์ž‘์„ฑํ•˜์˜€๋‹ค.

ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์ธํ•œ ํšŒ์›์˜ ์ •๋ณด ์กฐํšŒ, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ API๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

ํšŒ์›๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ์—์„œ DTO์—์„œ ์„ค์ •ํ–ˆ๋˜ Validation์˜ ํ˜•์‹ ์ ๊ฒ€์ด Error๊ฐ€ ์žˆ์„ ๋•Œ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด๋ฅผ ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.

@ApiImplicitParams({
            @ApiImplicitParam(name = "X-AUTH-TOKEN", value = "๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ํ† ํฐ", required = false, dataType = "String", paramType = "header")
    })

๋กœ๊ทธ์ธํ•œ ํšŒ์›์˜ ์ •๋ณด ์กฐํšŒ์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ API๋Š” JWT ํ† ํฐ ๊ฐ’์ด ์žˆ์–ด์•ผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

AccountService.java

package com.portfolio.backend.account;

// ... import ์ƒ๋žต

@RequiredArgsConstructor
@Transactional
@Service
public class AccountService {

    private final AccountRepository accountRepository;
    private final PasswordEncoder passwordEncoder;
    private final ResponseService responseService;
    private final JwtTokenProvider jwtTokenProvider;

    public CommonResponse signUp(SignUpRequest dto) {
        if(accountRepository.findByEmail(dto.getEmail()).isPresent()){
            throw new CustomValidationException("email-duplication");
        }

        Account account = Account.builder()
                .email(dto.getEmail())
                .nickname(dto.getNickname())
                .password(passwordEncoder.encode(dto.getPassword())) // ์•”ํ˜ธํ™”
                .email(dto.getEmail())
                .emailToken(UUID.randomUUID().toString()) // ์ด๋ฉ”์ผ ์ธ์ฆ ๋ฌธ์ž์—ด
                .isCertify(false) // ์ด๋ฉ”์ผ ์ธ์ฆ ์—ฌ๋ถ€
                .roles(Collections.singletonList("ROLE_USER"))
                .build();

        accountRepository.save(account);

        return responseService.getSuccessResponse();
    }

    public SingleResponse<String> signIn(SignInRequest dto) {
        Account account = accountRepository.findByEmail(dto.getEmail())
                .orElseThrow(CustomUserNotFoundException::new);
        if(!passwordEncoder.matches(dto.getPassword(),account.getPassword())){
            throw new CustomUserNotFoundException();
        }
        return responseService.getSingleResponse(jwtTokenProvider.createToken(account.getEmail(), account.getRoles()));
    }

    public SingleResponse<AccountInfoResponse> getAccountInfo(String email) {
        Account account = accountRepository.findByEmail(email)
                .orElseThrow(CustomUserNotFoundException::new);

        return responseService.getSingleResponse(new AccountInfoResponse(account));
    }

    public SingleResponse<String> updatePassword(PasswordUpdateRequest dto, String email) {
        Account account = accountRepository.findByEmail(email)
                .orElseThrow(CustomUserNotFoundException::new);
        if(!passwordEncoder.matches(dto.getBeforePassword(),account.getPassword())){
            throw new CustomUserNotFoundException();
        }
        account.changePassword(passwordEncoder.encode(dto.getUpdatePassword()));
        return responseService.getSingleResponse("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์™„๋ฃŒ");
    }
}

์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋งŒ๋“  API์—์„œ ํ˜ธ์ถœํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ์„œ๋น„์Šค๋‹จ์ด๋‹ค.

throw new CustomValidationException("email-duplication")

email์€ uniqueํ•ด์•ผ ํ•˜๋ฏ€๋กœ ํšŒ์› ๊ฐ€์ž… ์‹œ ์ค‘๋ณต๋œ ์ด๋ฉ”์ผ๋กœ ๊ฐ€์ž… ์‹œ๋„๋ฅผ ํ•˜๋ฉด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š”๋ฐ ์ด๋•Œ "email-duplication"๋ผ๋Š” ๋ฉ”์„ธ์ง€๋ฅผ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์— ๋ณด๋‚ด์ค€๋‹ค.

protected CommonResponse validationException(HttpServletRequest req, CustomValidationException e){
        if (e.getMessage() != null) {
            if (e.getMessage().equals("email-duplication")) return responseService.getFailResponse("์ค‘๋ณต๋˜๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.");
        }
        return responseService.getFailResponse("์ž˜ ๋ชป ๋œ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.");
    }

ExceptionAdvise์˜ validationException ๋ฉ”์†Œ๋“œ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด๋ฉ”์ผ ์ค‘๋ณต ๋ฉ”์„ธ์ง€์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค.

public interface AccountRepository extends JpaRepository<Account, Long> {
    Optional<Account> findAccountByEmail(String email);

    Optional<Account> findByEmail(String email);

    Optional<Account> findByNickname(String email);
}

AccountRepository์—๋„ Service ๊ฐ์ฒด์— ํ•„์š”ํ•œ ์ฟผ๋ฆฌ๋ฉ”์†Œ๋“œ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถ”๊ฐ€ํ•ด ์ฃผ์—ˆ๋‹ค.

์„œ๋น„์Šค๋‹จ์˜ ๋ชจ๋“  api response ๊ฐ์ฒด๋Š” ์•ž์„œ ๋งŒ๋“ค์—ˆ๋˜ model ๊ฐ์ฒด๋“ค๋กœ return ํ–ˆ๋‹ค.

responseService.getSingleResponse(jwtTokenProvider.createToken(account.getEmail(), account.getRoles()));

๋กœ๊ทธ์ธ api์—์„œ Request์—์„œ ๋ณด๋‚ธ ํšŒ์› ์ •๋ณด๊ฐ€ ๋งž๋Š”๋‹ค๋ฉด JWT ํ† ํฐ์„ ๋งŒ๋“ค์–ด์„œ ๋ณด๋‚ด์ฃผ๋Š” ์ฝ”๋“œ์ด๋‹ค.

Test Code & Swagger

๋‹ค์Œ๊ณผ ๊ฐ™์€ ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์•˜๋‹ค.

AccountControllerTest.java

package com.portfolio.backend.account;

// ... import ์ƒ๋žต

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class AccountControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @DisplayName("ํšŒ์›๊ฐ€์ž… ํ…Œ์ŠคํŠธ")
    @Test
    public void signupTest() throws Exception{

        // given
        SignUpRequest signupDto = SignUpRequest.builder()
                .email("SignUp@test.com")
                .nickname("signUp")
                .password("signUp1234")
                .build();

        // when
        final ResultActions perform = mockMvc.perform(post("/api/sign-up")
                .content(objectMapper.writeValueAsString(signupDto))
                .contentType(MediaType.APPLICATION_JSON));

        //then
        perform.andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Success"))
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.success").value(true));

    }

    @DisplayName("๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ")
    @Test
    public void signInTest() throws Exception{

        //given
        String email = "test@test.com";
        String nickname = "testUser";
        String password = "test1234";

        Account accountEntity = Account.builder()
                .email(email)
                .nickname(nickname)
                .password(passwordEncoder.encode(password))
                .isCertify(false)
                .roles(Collections.singletonList("ROLE_USER"))
                .emailToken(UUID.randomUUID().toString())
                .build();
        accountRepository.save(accountEntity);

        SignInRequest signInRequest = SignInRequest.builder()
                .email(email)
                .password(password)
                .build();

        //when
        final ResultActions perform = mockMvc.perform(post("/api/sign-in")
                .content(objectMapper.writeValueAsString(signInRequest))
                .contentType(MediaType.APPLICATION_JSON));

        //then
        perform.andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Success"))
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.data").exists());

    }

}


๋‘ ๊ฐ€์ง€ ํ…Œ์ŠคํŠธ๊ฐ€ ๋ชจ๋‘ ํ†ต๊ณผ๋๋‹ค.

Swagger

http://localhost:5000/swagger-ui.html๋กœ ์ ‘์†ํ•˜๋ฉด ์œ„์™€ ๊ฐ™์€ Swagger ํŽ˜์ด์ง€๊ฐ€ ๋‚˜์˜จ๋‹ค.

Controller ๊ฐ์ฒด์—์„œ ๋งŒ๋“  API๋“ค์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ณ  JSON์„ ์ž‘์„ฑํ•ด์„œ ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๋“ฑ์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.