저희 Handshake 서비스의 마이페이지는 유저의 기본적인 프로필 정보뿐만 아니라, 포지션, 기술 스택, 네트워킹 목적 등 여러 연관 테이블의 데이터를 모아서 보여줘야 합니다.
이번 포스팅에서는 마이페이지 조회 API를 구현하고, 서비스 레이어의 가독성을 높이기 위해 정적 팩토리 메서드(Static Factory Method) 로 리팩토링한 과정을 공유하려고 합니다.
가장 먼저 Controller, Service, Repository, Response DTO를 각각 작성하여 기능을 구현했습니다.
사용자 정보와 각 중간 테이블에서 가져올 ID 리스트를 담기 위해 Java의 Record 구조를 사용했습니다. 초기에는 @Builder를 사용하여 객체를 생성하도록 설계했습니다.
@Builder
public record ProfileResponse(
String nickname,
String profileImageUrl,
Boolean experience,
String career,
String dsti,
List<Long> positions,
List<Long> techSkills,
List<Long> networks,
String githubId,
String selfIntro
) { }
먼저 UserNetworkRepository, UserPositionRespository, UserTechSkillRepository 에서 사용자 본체의 정보를 가져오는 쿼리 메서드를 추가했습니다.
USerProfileService 에서는 연관된 모든 ID 리스트를 취합하는 로직을 작성했습니다.
@Transactional(readOnly = true)
public ProfileResponse getMyProfile(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND));
// 각 중간 테이블에서 ID 리스트 추출
List<Long> positionIds = userPositionRepository.findAllByUserId(userId).stream()
.map(up -> up.getPosition().getId()).toList();
List<Long> techSkillIds = userTechSkillRepository.findAllByUserId(userId).stream()
.map(up -> up.getTechSkill().getId()).toList();
List<Long> networkIds = userNetworkRepository.findAllByUserId(userId).stream()
.map(up -> up.getNetwork().getId()).toList();
// 빌더 패턴을 사용하여 DTO 생성 (리팩토링 전)
return ProfileResponse.builder()
.nickname(user.getNickname())
.profileImageUrl(user.getProfileImageUrl())
.experience(user.getExperience())
.career(user.getCareer())
.dsti(user.getDsti())
.positions(positionIds)
.techSkills(techSkillIds)
.networks(networkIds)
.githubId(user.getGithubId())
.selfIntro(user.getSelfIntro())
.build();
}
GET /user/info 엔드포인트를 생성해 현재 인증된 유저의 정보를 반환하도록 연결했습니다.
@GetMapping("/info")
public ResponseBody<ProfileResponse> getInfo(@AuthenticationPrincipal Long userId) {
ProfileResponse response = userProfileService.getMyProfile(userId);
return ResponseBody.success(response);
}
위와 같이 구현했을 때 기능상 문제는 없었지만, 팀원의 코드 리뷰를 통해 몇 가지 아쉬운 점을 발견했습니다.
서비스 로직의 비대화: 서비스 레이어에서 DTO의 필드를 하나하나 매핑하다 보니 코드가 길어지고 가독성이 떨어졌습니다.
캡슐화 부족: 응답 형식을 생성하는 책임이 서비스 레이어에 노출되어 있어, 필드가 추가되거나 변경될 때 서비스 코드를 매번 수정해야 했습니다.
디버깅의 어려움: 빌더가 길어질수록 어떤 데이터가 어디서 잘못 매핑되었는지 한눈에 파악하기 어려웠습니다.
이를 해결하기 위해 정적 팩토리 메서드(Static Factory Method) 구조로 리팩토링을 진행했습니다.
DTO 내부에 유저 엔티티와 ID 리스트들을 받아 직접 객체를 생성하는 from 메서드를 추가했습니다.
@Builder
public record ProfileResponse(
// 필드 생략
) {
public static ProfileResponse from(User user,
List<Long> positionIds,
List<Long> techSkillIds,
List<Long> networkIds) {
return ProfileResponse.builder()
.nickname(user.getNickname())
.profileImageUrl(user.getProfileImageUrl())
.experience(user.getExperience())
.career(user.getCareer())
.dsti(user.getDsti())
.positions(positionIds)
.techSkills(techSkillIds)
.networks(networkIds)
.githubId(user.getGithubId())
.selfIntro(user.getSelfIntro())
.build();
}
}
이제 서비스단에서는 복잡한 빌더 로직이 사라지고, 단 한 줄로 조회가 가능해졌습니다.
@Transactional(readOnly = true)
public ProfileResponse getMyProfile(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND));
List<Long> positionIds = getPositionIds(userId);
List<Long> techSkillIds = getTechSkillIds(userId);
List<Long> networkIds = getNetworkIds(userId);
// 정적 팩토리 메서드로 깔끔하게 반환
return ProfileResponse.from(user, positionIds, techSkillIds, networkIds);
}
이렇게 정적 팩토리 메서드로 리팩토링을 해서 얻은 장점은 여러가지가 있습니다.
- 명확한 의도 표현:
from()이라는 이름만 봐도 "User 객체로부터 Response DTO를 만든다"는 의도가 직관적으로 전달됩니다.- 변환 책임의 캡슐화: 엔티티를 DTO로 변환하는 상세 로직이 DTO 내부에 숨겨져 있어 서비스 코드가 간결해집니다.
- Builder를 통한 가독성과 유연성: 메서드 내부에서는 Builder를 사용하므로 필드가 많아도 가독성이 유지되며, 파라미터 순서로 인한 실수를 방지할 수 있습니다.
- 유지보수의 용이성: 필드가 추가되거나 변경되어도 DTO 내부의
from메서드만 수정하면 됩니다. 서비스 레이어까지 영향이 가지 않습니다.- 명시적인 설계 활용:
of,from등 관례적인 이름을 사용하여 다른 개발자가 코드를 볼 때도 의도를 쉽게 파악할 수 있습니다.
이번 리팩토링을 통해 단순히 기능을 구현하는 것을 넘어, "어떻게 하면 더 읽기 좋고 관리하기 쉬운 코드를 만들 것인가"에 대해 깊이 고민해 볼 수 있었습니다.