이번 학기에 캡스톤 프로젝트로 '생성형 AI를 활용한 스타일링 및 상품 추천 서비스'라는 주제를 선정해서 진행하고 있다.
여기서 AI 모델 AWS 배포 및 백엔드 서버 구축 등의 역할 맡았는데, 자세한 내용은 프로젝트가 끝나고 나서 글로 풀어보려고 한다.
이 프로젝트에서 생성형 이미지를 얻기 위해서는 사용자의 이미지들을 여러 장 AI 모델에게 업로드 해서 넘겨주어야 한다.
바로 AI 모델에게 넘겨준다면 그냥 AWS S3로 업로드 하고, 저장한 후 각각의 S3 URI를 프론트에게 다시 넘겨주는 방식으로 간단하게 해결할 수 있다.
그런데 우리 프로젝트의 경우 회원 가입이나 회원 정보 수정을 통해 미리 사용자의 이미지 여러 장을 저장해는 방식으로 사용자 인터페이스를 설계했다.
따라서 데이터베이스에 미리 이미지들을 저장해두고 필요할 때 호출하는 방식으로 접근해야 할 것 같다고 판단했다.
다만 RDB(MySQL)에 List<String>
을 어떻게 저장할 것인지에 대한 문제가 생겼다.
이에 대한 방법은 크게 4가지 정도 찾을 수 있었다.
이 방법은 "[", "," 등의 String 파싱을 해야 한다는 단점이 있어서 깊게 고려해보지 않았다.
List<Image>
로 처리하는 방법도 있다.이 방법의 경우 '굳이 데이터베이스의 join 연산을 사용하면서 이미지들을 불러오는 게 효율적인가?'라는 생각이 들어서 후순위로 넘겼다.
이 방법의 경우 MySQL에서 지원해주는 자료 타입을 사용하니까 1번 방법보다 훨씬 단순하게 해결할 수 있다는 장점이 있다.
Json 데이터를 저장할 때 사용할 수 있는 타입은 Json 타입과 Text 타입이 있다. MySQL은 TEXT든 JSON이든 데이터 값이 크기 대문에 데이터를 읽기 위해서는 external page에 접근한다. 그런데 Json의 특정 필드 값에 접근하려면 Json 파싱까지 추가적인 작업을 수행한다. 특정 Json 값에 접근하는 쿼리가 있다면 TEXT와는 차이가 크다. 왜냐하면 파싱 하는 작업이 추가로 들기 때문이다. 즉, 변환 작업이 훨씬 복잡해서 오래 걸린다.
다만 이 경우에도 위와 같이 RDB에서 Json 파싱 작업을 거쳐야 하기 때문에 마찬가지로 일단 보류하기로 했다.
'DB에서 Json 파싱하는 것보다 백엔드 서버에서 Json으로 역직렬화 하는게 좀더 효율적이지 않을까..?' 라는 생각으로 4번의 방법을 선택하게 되었다.
실제 구현은 아래와 같이 했다.
@RequiredArgsConstructor
@Transactional
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private final ObjectMapper objectMapper;
@Transactional(readOnly = true)
public UserInfoResponse getUserInfo(Long id) throws JsonProcessingException {
final User user = userRepository.findById(id).orElseThrow(
() -> new UsernameNotFoundException("User not found")
);
List<String> imgUrls = deserializeImageUrls(user);
return UserInfoResponse.of(user, imgUrls);
}
private List<String> deserializeImageUrls(User user) throws JsonProcessingException {
return objectMapper.readValue(user.getUserImages(), new TypeReference<List<String>>() {});
}
}
테스트 코드는 다음과 같이 작성하였다.
@DisplayName("회원 정보를 조회한다.")
@Test
void getUserInfo() throws JsonProcessingException {
//given
final User user = userRepository.findByEmail(EMAIL).orElseThrow();
//when
final UserInfoResponse userInfoResponse = userService.getUserInfo(user.getId());
//then
assertThat(userInfoResponse.email()).isEqualTo(EMAIL);
assertThat(userInfoResponse.nickname()).isEqualTo(NICKNAME);
assertThat(userInfoResponse.images())
.hasSize(2)
.containsExactlyInAnyOrder("image1.png", "image2.png");
}
테스트 코드를 작성하면서 'private 메서드도 테스트 코드를 작성할까?'라는 생각도 들었지만 검색을 조금 해보니까 private 메서드에 대한 테스트 코드는 추천하지 않는다는 글이 대다수였다.
그리고 사실 getUserInfo()
테스트 코드에 이미 private 메서드에 대한 테스트를 포함하는 거니까 굳이 따로 작성할 필요가 없다는 생각도 들어서 따로 작성하지 않았다.
또 생각해보니까 deserializeImageUrls()
를 만든 이유도 코드의 가독성을 높이기 위해서여서 따로 테스트 코드는 작성하지 않았다.
이번에 프로젝트를 진행하면서 저번 프로젝트와는 다르게 테스트 코드를 많이 작성하는 것을 목표로 정했다.(커버리지 80% 이상)
실제로 테스트 코드를 작성하면서 장점도 많이 느끼고 있는데(빼먹은 논리 구조 발견, 코드 안정성 증가, 유지 보수성의 편리함 등)
이에 대한 자세한 글도 프로젝트가 끝나고 다시 한번 적어봐야 할 것 같다.