
최주호 강사님의 인프런 강좌 정리 및 실습한 기록
@Service 생성@RestConroller 생성Database -> Service -> Controller 순으로 생성한다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AccountService {
private final AppUserRepository appUserRepository;
private final AccountRepository accountRepository;
@Transactional
public AccountSaveResponseDTO registerAccount(AccountSaveRequestDTO accountSaveRequestDTO, Long appUserId) {
// AppUser 가 DB 에 존재하는지 검증
// --- 빈칸을 채우시오 (1) ---
// 해당 계좌가 DB 에 존재하는 지 검증
// --- 빈칸을 채우시오 (2) ---
// DB 에 계좌 등록
// DTO 를 Entity 로 변경
// --- 빈칸을 채우시오 (3) ---
// DTO 응답
// --- 빈칸을 채우시오 (4) ---
}
}
빈칸을 채워보겠습니다. 현재 상황을 한번 점검해보자면
spring data jpa 가 적용되어 있습니다.JPA 에 대한 기본적인 지식이 있어야 작성이 가능합니다.
DTO 는 하나의 클래스에서 관리합니다.
inner static class 로 필요한 DTO 를 계속 추가해나갑니다.
@Valid 를 통한 간단한 유효성 검증만 적용되어 있습니다.
public class AccountResponseDTO {
@Getter
@Setter
public static class AccountSaveResponseDTO {
private Long id;
private Long number;
private Long balance;
// 생성자
public AccountSaveResponseDTO(Account account) {
this.id = account.getId();
this.number = account.getNumber();
this.balance = account.getBalance();
}
}
}
Account 를 저장하고자 요청하는 DTO 객체입니다.
간단한 테스트 목적으로 작성되었으므로, 길이를 4로 고정합니다.
public class AccountRequestDTO {
@Getter
@Setter
public static class AccountSaveRequestDTO {
@NotNull
@Digits(integer = 4, fraction = 4) // 최소4자, 최대 4자
private Long number;
@NotNull
@Digits(integer = 4, fraction = 4)
private Long password;
public Account toEntity(AppUser appUser) {
return Account.builder()
.number(number)
.password(password)
.balance(1000L)
.appUser(appUser)
.build();
}
}
}
JPA 를 알고 있다면 save, findBy... 으로 별다른 로직없이 작성할 수 있습니다.
강의에서는 JPA 가 기본적으로 제공하는 기능들에 대해서는 테스트하지 않습니다.
추후 fetch join 에 관한 리팩토링이 이루어집니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AccountService {
private final AppUserRepository appUserRepository;
private final AccountRepository accountRepository;
@Transactional
public AccountSaveResponseDTO registerAccount(AccountSaveRequestDTO accountSaveRequestDTO, Long appUserId) {
// AppUser 가 DB 에 존재하는지 검증
AppUser appUserPS = appUserRepository.findById(appUserId).orElseThrow(
() -> new CustomApiException("해당 유저를 찾을 수 없습니다.")
);
// 해당 계좌가 DB 에 존재하는 지 검증
Optional<Account> account = accountRepository.findByNumber(accountSaveRequestDTO.getNumber());
if (account.isPresent()) {
throw new CustomApiException("해당 계좌가 이미 존재합니다.");
}
// DB 에 계좌 등록
// DTO 를 Entity 로 변경
Account accountPS = accountRepository.save(accountSaveRequestDTO.toEntity(appUserPS));
// DTO 응답
return new AccountSaveResponseDTO(accountPS);
}
}
로그인을 한 유저가 계좌를 등록하려고 합니다.
컨트롤러에서는 어떤 인자가 필요할까요?
현재 AOP 가 적용되어 있습니다. 그리고 1명의 유저가 n개의 계좌를 개설할 수 있습니다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class AccountController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final AccountService accountService;
@PostMapping("/account/save")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<?> saveAccount(
@RequestBody @Valid AccountSaveRequestDTO accountSaveRequestDTO,
BindingResult bindingResult,
@AuthenticationPrincipal LoginAppUser loginAppUser
) {
AccountSaveResponseDTO accountSaveResponseDTO = accountService.registerAccount(accountSaveRequestDTO, loginAppUser.getAppUser().getId());
return new ResponseEntity<>(new ResponseDTO<>(1, "계좌를 성공적으로 등록했습니다.", accountSaveResponseDTO), HttpStatus.CREATED);
}
}

AOP 가 적용되어 BindingResult 에 에러가 담기고, 계좌 정보와 사용자 정보가 컨트롤러가 전달되는 상황입니다.
이전까지가 회원가입, 로그인이었다면, 그 이후에 사용자의 요청에는 매번 token 이 필요합니다.
회원가입의 간편화를 위해서 CommandLineRunner 를 사용합니다.
@Configuration
public class DummyDevInit extends DummyObject {
@Bean
@Profile("dev") // prod 에서는 실행되지 않도록 특정한 profile 을 지정한다.
CommandLineRunner init(AppUserRepository appUserRepository) {
return (args) -> {
// 서버 실행 시 무조건 실행된다.
appUserRepository.save(newAppUser("ssar", "bori"));
};
}
}
AppUser 와 동일하게 newAccount, newMockAccount 를 생성합니다.
Dummy 는 @Test 환경에서 실제 데이터가 아닌, 테스트 용 샘플 데이터 생성에 사용됩니다.
/*
* 편의를 위해 비밀번호는 1234 고정
* 해당 객체는 테스트 시 DB 에 저장하기 위한 테스트 객체이다.
*
* */
protected Account newAccount(Long number, AppUser appUser) {
return Account.builder()
.number(number)
.password(1234L)
.balance(1000L)
.appUser(appUser)
.build();
}
/*
* DB 에서 select 시 사용되는 테스트 객체이다.
*
* */
protected Account newMockAccount(Long id, Long number, Long balance, AppUser appUser) {
return Account.builder()
.id(id)
.number(number)
.password(1234L)
.balance(balance)
.appUser(appUser)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
}
@SpringBootTest 는 통합 테스트를 위한 어노테이션입니다. 스프링 환경을 전부 메모리에 띄우므로 단위 테스트에서는 사용이 권장되지 않습니다.
@ExtendWith 어노테이션을 사용하여 Mockito 환경만을 불러옵니다.
@ExtendWith(MockitoExtension.class)
public class AccountServiceTest() extends DummyObject {
// ...
}
의존성 주입
계좌 등록을 위해서 AppUser, Account repo 를 불러옵니다.
@InjectMocks // 모든 Mock 들이 @InjectMocks 로 주입된다.
private AccountService accountService;
@Mock // 가짜 repo
private AccountRepository accountRepository;
@Mock
private AppUserRepository appUserRepository;
@Spy // 가짜 환경에 주입되는 진짜 객체
private ObjectMapper objectMapper;
이후, 계좌 생성에 필요한 계좌 요청 정보, 사용자 정보 를 생성합니다.
계좌를 repo 에 등록 후, 해당 계좌의 정보가 방금 생성한 계좌의 정보와 일치하는 지 테스트하는 사실 크게 의미는 없는 테스트 입니다.
Mockito 테스트에 익숙해지기 위한 여러 과정 중 하나라고 생각하시면 될 것 같습니다.
@Test
public void account_register_test() throws JsonProcessingException {
/* given */
Long appUserId = 1L;
AccountSaveRequestDTO accountSaveRequestDTO = new AccountSaveRequestDTO();
accountSaveRequestDTO.setNumber(1000L);
accountSaveRequestDTO.setPassword(1234L);
/* stub 1 */
AppUser appUser = newMockAppUser(appUserId, "ssar", "bori");
when(appUserRepository.findById(any())).thenReturn(Optional.of(appUser));
/* stub 2 */
when(accountRepository.findByNumber(any())).thenReturn(Optional.empty());
/* stub 3*/
Account account = newMockAccount(1L, 1000L, 1000L, appUser);
when(accountRepository.save(any())).thenReturn(account);
AccountSaveResponseDTO accountSaveResponseDTO = accountService.registerAccount(accountSaveRequestDTO, appUserId);
String responseBody = objectMapper.writeValueAsString(accountSaveResponseDTO);
System.out.println(responseBody);
/* then */
Assertions.assertThat(accountSaveResponseDTO.getNumber()).isEqualTo(1000L);
}
인텔리제이에서 제공하는 http 를 활용해서 테스트 해보겠습니다.
로그인 후 생성되는 토큰을 global 변수로 활용해서 복사, 붙여넣는 과정을 생략합니다.
JUnit 의 테스트와 마찬가지로 개별 테스트, 통합 테스트 처럼 실행할 수 있습니다.
위에서 생성한 CommandLineRunner 로 서버 실행 순간에 sample 유저가 회원가입 됩니다.

client.log 를 통해서 콘솔에 출력할 수 있는데, 이는 상단에 Response Handler 탭에서 확인할 수 있습니다.
http header 에 'Authorization' 으로 담긴 'Bearer 어쩌고저쩌고' 를 global 변수로 등록합니다. 세션이 살아있는 동안에 유지됩니다.
POST 요청을 보내면, 다음과 같이 JWT 가 생성됩니다.

다음과 같이 client.test 를 통해서 .http 파일 내에서 테스트를 진행할 수 있습니다.
이 사이트 에서 좀 더 자세한 내용을 확인할 수 있습니다.
예시처럼 간단한 테스트 말고, 꽤 정교한 테스트도 가능해보입니다.

토큰 길이가 -1 이라고 놓고 요청을 POST 요청을 보내보면 Tests 탭에 다음과 같이 나옵니다.

위에서 만들어진 JWT 를 사용해봅시다. 사용하는 방법은 {{ ... }} 입니다.

이제 생성된 토큰을 매번 복사, 붙여넣기 할 수고를 덜 수 있게 되었습니다!

.env 파일을 만들어서 민감한 정보를 가릴 수 있습니다..http 파일로 만들기 때문에, 버전 관리를 할 수 있습니다 😄HTTP 요청과 동시에 결과값을 검증할 수 있습니다.