ํ๋ก์ ํธ ๊ธฐ๋ณธ ์ธํ
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์ ์์ฑํด์ ํ์๊ฐ์
, ๋ก๊ทธ์ธ ๋ฑ์ ํ
์คํธ๊ฐ ๊ฐ๋ฅํ๋ค.