회고

캡스톤 디자인 회고

gucoding 2025. 5. 25. 01:50

정리하기

지금까지 했던 프로젝트는 단순히 미니 프로젝트이거나 개인적인 실력의 한계 때문에 제대로된 코딩을 못한채 흐지부지 해왔습니다.
그래서 학교 강의겸해서 진행한 캡스톤 디자인이 사실상 제 첫 프로젝트가 되었습니다.
여러모로 아쉬운점도 있고 깨달은점도 있어서 다음 프로젝트를 진행할 때 참고하고자 남겨봅니다.

프로젝트 소개

서버쪽은 다른 팀원 한 분과 같이 진행했습니다. 중고경매 어플리케이션을 개발했고 제가 맡은 기능은
인증(인가), 회원, 채팅, 포인트, 기타 외부 api(소셜로그인, 결제, 배송) 입니다.
아키텍처를 간단하게 소개하면 기존 레이어드 아키텍처에서 도구레이어를 추했습니다.

아키텍처 : 어떤 비즈니스 문제를 해결하기 위해, 준수해야하는 제약을 넣는 과정 (김우근)

코드를 좀 더 깨끗하게 작성하려면 비즈니스로직과 도메인로직을 구분하자라고 평소에는 이정도로 인식하고 있었는데요, 이런 고민을 해당 개발자님께서는 본인이 정의한 계층을 추가해서 문제를 해결했던게 인상깊어서 차용했습니다.

도메인 엔티티와 jpa 엔티티를 분리하면서, dao계층에서(인터페이스 구현한) jpaRepository를 컴포지션으로 가지는 형태입니다.

테스트코드는 리포지토리, 서비스 계층을 실제 객체를 구현하는 식(Classicist)으로, 컨트롤러 계층을 Mockito 프레임워크의 도움을 받았고, API문서는 RestDocs로 작성했습니다. 테스트의 픽스쳐는 Fixture Monkey를 사용해 모킹객체를 쉽게 만들었습니다.

아쉬운 점

테스트코드

가장 불편함을 느꼈던 부분은 테스트코드였습니다. 테스트코드도 처음 짜봐서 미숙한 주제에 Classicist으로 해결하려다보니, 당장 수정해서 배포해야되는 상황에서 테스트 때문에 걸림돌이 되었습니다.

특히나 커뮤니케이션이 미숙한 나머지 같은 기능의 response값을 여러번 바꿔야될 때, 번거로웠던 기억이 납니다. 급해지다보니 테스트코드도 대충짜고 그러다보니 예상치못한 오류가 발생하고, 최악의 사이클이 만들어졌네요 (소통 또한 다듬어나가야할 부분…)

그래서 당분간 제 테스트를 짜는 실력을 올리기 전까지는 Mockito 프레임워크의 도움을 더 받아야겠습니다. 그리고 최근에 우연히 groovy로 테스트코드를 짜는 Spock 프레임워크를 사용해봤는데, Mock이라든지 given,when,then 문법등이 내장되어있는 등 전반적으로 기존 JUnit 기반 테스트보다 사용하기 편리했습니다. 자바랑도 호환가능한데 문법도 별로 어렵지 않아서 다음에 사용할 예정입니다.

도메인 설계

중고경매 시스템에 USEDITEM(중고물품), AUCTIONITEM(경매물품) 두 도메인이 있습니다. 설계에 대한 고민없이 무지성으로 언젠가는 다른 로직이 들어갈 수 있으니 분리해야겠다고 생각하고 개발했습니다. 그러다가 다음 요구사항이 들어왔습니다.

  • UsedItem이랑 AuctionItem처럼 서로 다른 테이블에서 가져와야 하는 데이터를, itemType에 따라 분기해서 조회
if (itemType == ItemType.USEDITEM) {
            chatRoomMetaData = jpaQueryFactory.select(Projections.constructor(
                    ChatRoomMetaData.class,
                    usedItemEntity.usedItemTransactionEntity.transactionMode,
                    usedItemEntity.itemDetailsEntity.title,
                    usedItemEntity.itemDetailsEntity.price,
                    profileImageEntity.name
                    )).from(usedItemEntity)
                    .where(usedItemEntity.id.eq(itemId))
                    .join(profileImageEntity).on(profileImageEntity.memberId.eq(otherPersonId))
                    .fetchFirst();
        } else {
                    ...
        }

QueryDSL을 사용했습니다. from 대상 자체를 동적으로 바꿀 수 없다보니 if-else로 유사한 쿼리를 각각 짰는데요 이걸짜면서 뭔가 이상함을 느꼈습니다. 처음에는 QueryDsl 문젠가 싶으면서, 어쨌건 네이티브 쿼리도 UNION을 쓰면서 다소 더럽다고 느껴지더라구요. 애초에 단일테이블로 구성했다면 어땠을까 싶습니다.

그렇다면 어떻게 판단해야됐을까요? 일단은 아무래도 경험이 부족하다 요구사항과 UI를 보면서 판단해볼 생각을 못했고 각 도메인의 속성 유사도와 추후에 달라질 여지가 있을지에 대해서 계속 고민하는 연습을 해야할 것 같습니다.

Projection vs 조합

public class ProfileImage {
    private Long memberId;
    private String name;
    private String imageUrl;
}

@Getter
@AllArgsConstructor
public class ProfileResponse {
    private String imageName;
    private String imageUrl;
    private String nickname;
    private int point;
}

다음 Response를 만들기위해서는

ProfileImage, MemberPoint, Member, 3개의 엔티티를 조회해야했습니다.

소셜로그인 시 제공되는 프로필이 있고 사용자가 따로 프로필 이미지를 등록할 수 있을 때,

프로필 이미지를 조회할 때, 우선순위를 정해야했습니다.

(소셜프로필은 url을 그대로 사용하면 조회할 수 있고, 사용자프로필은 name을 통해 s3에서 조회하는 형식입니다.)

닉네임같은 경우에는 뒤에 태그 (e.g. 닉네임#1234)를 제외해야합니다.

public ProfileResponse getMyProfile(Long memberId) {
        return queryFactory.select(Projections.constructor(ProfileResponse.class,
                        profileImageEntity.name,
                        profileImageEntity.imageUrl, // locate(substring, string)
                        Expressions.stringTemplate("substring({0}, 1, locate('#', {0}) - 1)", // string, start_position(default=1), length(가져올 문자 수)
                                memberEntity.memberPrivateInformationEntity.nickname),
                        memberPointEntity.point,
                        profileImageEntity.isSocialProfileVisible
                        ))
                .from(memberEntity)
                .where(memberEntity.id.eq(memberId))
                .leftJoin(memberPointEntity).on(memberEntity.id.eq(memberPointEntity.memberId))
                .leftJoin(profileImageEntity).on(memberEntity.id.eq(profileImageEntity.memberId))
                .fetchFirst();

    }

다음 쿼리를 짜면서 이런 생각이 들었습니다. 도메인 로직에 영향이 가는 dto인 경우에는 따로 조회해서 조합하는 식으로 풀어가는게 코드 이해도 좋고, 유지보수에 좋을 것 같다는 생각이 듭니다.

String getImage() {
    return ImageUrl != null ? ImageUrl : ImageName
}

public String getRawNickname() {
   int i = nickname.indexOf("#");
   return nickname.substring(0,i);
}

이런식으로 자바코드에서 이미지 분기처리를 한다던가 제공할 닉네임 로직을 짜는등의 도메인 로직을 둔다면,

정책이 바뀌었을 때 쉽게 바꿀 수 있을 것 같습니다.

기타

실시간으로 로그를 찍어보려고 ec2에 들어갔는데, 프론트분이 실수로 몇천개의 요청을 보내서 한참동안 못봤던 기억이 납니다. 로그 관리 시스템을 구축해봐야겠습니다.

어떤 악성해킹이 지속적으로 요청을 보내더라구요. cloudfront에서 국내 IP만 받도록 지정했습니다. 다음에는 사전에 구성해야 할 듯 하고 과도한 요청을 제한하도록 구성해야 할 듯합니다.

record라는걸 알고나서 처음에는 낯설어서 쓰지 않았는데, 코틀린을 배우면서 코드양을 줄여보자는 욕심이 듭니다. 다음에는 dto 구성 시 적극적으로 채용해보도록합니다.

코틀린을 최근부터 조금씩 보기 시작했습니다. 자바에 비해 짧고 가독성 좋고, 코루틴 지원받는 언어라 비동기 처리시 간편하게 작성할 수 있는 등 장점이 많은 언어로 알고 있습니다. 조금씩 학습해서 다다음 프로젝트에 적용하고픈 마음입니다.