졸업작품 Dandi 개발일지 #3

공병주(Chris)·2023년 3월 31일
0

api 및 기능

  • 게시글
    • 게시글 이미지 등록
    • 작성
    • 상세보기
    • 기온에 따른 모든 사용자의 게시글 목록 조회
    • 기온에 따른 내 게시글 목록 조회
    • 삭제
  • 게시글 좋아요
    • 등록 / 취소
  • 로그아웃
  • 옷장
    • 옷 등록
    • 옷 삭제
    • 카테고리, 계절에 따른 옷 조회

Infra

  • CD(Github Actions, Code Deploy, S3)

Github Actions, S3, Code Deploy를 통한 CD 환경 구축

CI는 이전에 Github Actions으로 구축해두었다. 이제 슬슬 iOS 개발자가 서버와의 연결을 한다고 해서 CD를 해두어야 하는 상황이었다. 따라서, Github Actions, S3, Code Deploy을 통해서 CD 환경을 구축해두었다.

위 흐름대로 CD가 일어난다. 또한, CD의 결과를 Slack으로 전송하도록 구축해두었다.

착장에 대한 느낌

게시글을 작성할 때, 착장을 등록하고 오늘 이 착장이 어땠는지에 대한 정보를 입력한다.

  • 너무 추워요
  • 추워요
  • 딱 좋아요
  • 더워요
  • 너무 더워요

위 값들은 사용자가 변경할 수 없고, 서비스에서 제공하는 포맷이다.

이 값을 서버에서 처리할 때, 아래 2가지를 고민했다.

  1. 너무 춥다, 춥다 등의 정보를 알아야 한다.
  2. 단순하게 착장 느낌에 대한 Index만 알면 된다.

2로 결론을 지었다.

일단, 서버에서 Index에 해당하는 착장 느낌이 뭔지 알아야 제공할 수 있는 기능이 없다. 특정 느낌에 대한 조회 API가 있어도, 착장 느낌의 클라이언트에서 착장 느낌 Index를 포함한 조회 요청을 하면 필터링 해서 응답해주면 된다.

또한, 위 착장에 대한 느낌이 현재는 5개이지만, 앞으로 개수가 증가해도 변경할 일이 없다. Enum으로 착장 느낌에 대한 정보를 구체적으로 관리하면 착장 느낌에 대한 개수가 증가한다면 Enum 타입을 계속 추가해줘야 할 것이다. 하지만, Index 값으로만 관리한다면 이는 숫자에 불과하기 때문에 타입의 표현범위 내에서 무한히 확장할 수 있다.

그렇다면, 이를 클라이언트에서 (1, 너무 추워요), (2, 추워요) 와 같은 Map의 형태로 관리해야할 것이다. 결국, 클라이언트가 이를 관리할 것인가 서버가 이를 관리할 것인가에 대한 이야기로 이어질 것 같다.

만약, 너무 추워요라는 글이 정말 추워요라는 글으로 변경된다면 어떨까? 서버 사이드 관리의 경우에는, 사용자에게 보여질 값의 변경에 따라서, 서버에 변경이 일어날 것이다. 이는 철저히 View의 변경이라고 생각한다. 현대의 웹/앱 개발이 Client-Server 구조로 나눠짐에 따라 책임지는 부분이 있는데, View의 변경은 Client의 역할라고 생각한다.

만약, 단순 사용자에게 보여질 텍스트가 아니라 기능적으로 Index 값에 대한 변경이 일어난다고 생각해보아도, Index로 관리하는 것에 문제될 것이 없다고 생각한다.

게시글 저장 기능

헥사고날 아키텍처의 장점은 DB 주도 설계를 방지할 수 있다는 것이다. 다시 말해서, JPA 기술을 사용하는 환경에서 JPA Entity와 도메인을 분리할 수 있다. 도메인에서 필요없는 객체를 의존할 필요가 없다는 것이다.

게시글 상세 조회 요청에 대한 응답을 할 때, 작성자의 닉네임을 함께 반환해야한다면 아래와 같이 설계할 것이다.

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "memberId")
    private Member member;

    // 생성자

    public String getWriterNickname() {
        return member.getNickname();
    }
}

도메인을 JPA Entity로 사용했다면 위와 같이 도메인을 설계해야할 것이다.

혹은 Member와의 연관관계를 끊는다면 아래와 같이 id를 통한 간접참조를 하고 memberId로 member 테이블을 조회해서 nickname을 조회해야할 것이다.

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private Long id;

    private Long memberId;

    // 생성자

    public Long getMemberId() {
        return member.getId();
    }
}

하지만, 아래처럼 현재 아키텍처에서 도메인은 JPA Entity가 아닌 POJO이고 영속화를 위한 JPA Entity는 PostJpaEntity 형식으로 따로 구현해두었다.

public class Post {

    private final Long id;
    private final Member writer;
    private final Temperatures temperatures;
    private final WeatherFeeling weatherFeeling;
    // ...

}

위 Post 도메인을 영속화할 때 어떻게 해야할까? 우선 Client에서 들어오는 정보는 memberId와 등록할 Post에 대한 정보다.

게시글 작성 기능을 구현하다가 든 고민이다.

아래와 같이 생긴 PostJpaEntity를 보자. 객체 이름이 조금 이상하게 보일 수 있다. 현재 아키텍처에서 도메인은 JPA Entity가 아닌 POJO이고 영속화를 위한 JPA Entity 아래처럼 따로 구현해두었다.

@Entity
@Table(name = "post")
public class PostJpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "memberId")
    private MemberJpaEntity memberJpaEntity;

    @OneToMany(mappedBy = "postJpaEntity")
    @Cascade(value = CascadeType.ALL)
    private List<AdditionalFeelingIndexJpaEntity> additionalFeelingIndicesJpaEntities;

    public PostJpaEntity fromPost(Post post) {
        // ...
    }
    
    // ...
}

1. 위처럼, PostJpaEntity가 MemberJpaEntity를 참조하게 하는 것이 좋을까?

2. 아래처럼, memberId를 통해 간접 참조하게 하는 것이 좋을까?

@Entity
@Table(name = "post")
public class PostJpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private Long id;

    private Long memberId;

    @OneToMany(mappedBy = "postJpaEntity")
    @Cascade(value = CascadeType.ALL)
    private List<AdditionalFeelingIndexJpaEntity> additionalFeelingIndicesJpaEntities;

    public PostJpaEntity fromPost(Post post) {
        // ...
    }
    
    // ...
}

1번 방식

PostMemberJpaEntity를 영속화 하기 전에, MemberJpaEntity를 조회해서 PostMemberJpaEntity에 넣어줘야한다. 따라서, MemberJpaEntity를 1번 Select 해야한다.

PostJpaEntity를 save하는 과정에서 member를 select하는 쿼리 1번, post를 insert하는 쿼리 1번, AdditionalFeelingIndexJpaEntity의 개수(n)에 따라 n번의 insert쿼리가 n + 2번의 쿼리가 실행한다.

조회시에는, post와 AditionalFeelingIndex를 select 하는 쿼리 1번, Member를 select 하는 쿼리 1번이 발생한다. 총 2번의 쿼리가 실행다.

  • Post Save 시점 : select 1번, insert n + 1번
  • Post Read 시점 : select 2번

2번 방식

2번의 방식은 Post를 조회할 때, memberId를 꺼내서 Member의 nickname을 한번 더 조회해야한다.

PostJpaEntity를 Save하는 과정에서는 MemberJpaEntity와의 연관관계를 맺지 않고 단순하게 memberId 값만을 가진 상태로 save만 하면 되니까 1번의 쿼리가 실행된다. 거기에 n개의 AdditionalFeelingIndexJpaEntity를 insert하는 n번의 쿼리까지 n + 1번의 쿼리가 실행된다.

Post와 AdditionalFeelingIndex를 select 하는 쿼리 1번, Member의 정보를 read 하는 쿼리 1번으로 2번의 쿼리가 발생한다.

  • Post Save 시점 : insert n + 1번
  • Post Read 시점 : select 2번

어떤 방식이 더 나을까?

일단, save 시점에서 1번의 select 쿼리를 실행하지 않을 수 있기 때문에 1번 방식이 좋아보인다.

또한, 1번 방식의 단점은 필요한 데이터는 Member의 nickname인데, Member의 모든 데이터가 다 조회가 된다는 것이다. 불필요한 데이터가 함께 조회되는 것이다.

memberId에 대한 검증

memberId로 MemberJpaEntity를 조회하면서 memberId에 대한 검증을 해야하지 않나..? 라는 생각도 들었다. 하지만, memberId는 Token에서 추출된 정보이다. 그렇다면, memberId가 유효하지 않다는 것은 Token이 조작되었다는 것이다. Token 조작 가능성에 대해 고민해보았다.

먼저, Token이 조작되어서 Long 타입의 값을 payload로 추출할 수 없다면 이는 Interceptor에서 처리가 된다.

다음으로, Token이 조작되었는데 Long 타입의 값을 payload로 추출할 수 있는 상황이라면 어떨까? 그렇다면 누군가가 jwt private key를 해킹했다는 말인데, private key는 github의 private repo에 올라가있기 때문에 가능성이 적다고 판단해서 memberId에 대한 검증을 하지 않았다.

DataSource를 통해 Table 이름 추출하는 방법

통합테스트에서 테스트마다 DB를 초기화해주었는데, 아키텍처를 바꾸면서 이전의 초기화 방식을 사용하지 못해서 새로운 방식으로 초기화를 했다.

DataSource를 통해 Table 이름 추출하는 방법

View에서 필요한 값만 조회하는 메서드와 조회 전용 DTO에 대하여

조회 로직이 다양하고 조회마다 필요한 값들이 다르다. 이에 따라, DB에서 데이터를 어떻게 조회할 지에 대한 고민들을 해봤다.

View에서 필요한 값만 조회하는 메서드와 조회 전용 DTO에 대하여

좋은 팀원이 되는 것에 대해

어떻게 하면 팀원과 좋은 관계를 유지할 수 있을까?

이번에 팀원과 작은 갈등이 있었다.

갈등의 원인은 API 스펙이었다. 서버 개발자인 내가 API를 개발하고 Swagger를 작성하고 클라이언트 개발자가 Swagger 문서를 보고 서버와 연결하는 방식이다.

클라이언트 개발자가 Swagger 문서를 보고 API 스펙을 조금만 바꿔달라는 요청들이 몇번 있었다.

나는 최대한 HTTP 기본 규약들에 맞춰서 REST하게 개발하고 싶었다. 하지만, 팀원은 REST를 추구하는 것은 좋지만, 본인이 제시하는 방식으로 개발 생산성이 조금 높아질 수 있기에 덜 REST 하더라도 변경을 해줬으면 좋겠다는 의견이었다. 물론, 덜 REST 하더라도 생산성이 높아지면 좋을 수 있다. 하지만, 몇가지 이유들 때문에 rfc 문서와 www 문서들을 함께 거절의 의사를 밝혔다. 과정에서 서로 마음이 상했던터라, 오가는 말투들이 조금은 딱딱해지기 시작했다. 그렇게 어떻게 대화는 마무리 되었고, 다음날 팀원과 이야기를 나눴다.

상호 피드백 후에 내가 느낀 점은 아래와 같다.

1. 나는 개발하는 기계가 아니라 함께 개발을 하는 사람임을 기억하자.

너무 REST와 HTTP 기본 규약에 집착을 했다. 사실 API 규변경에 응하면 팀원은 더 쉽게 개발할 수 있고, 서버에 성능적인 문제도 없을 뿐더러, 변경을 하는데 드는 시간도 크지 않았다.

그런데, 난 왜 이렇게 rfc, www 문서에 나온 내용들에 집착했을까..?

이런 규약과 같은 것을 넘어서, 앞으로는 크리티컬 한 문제가 발생하는 것이 아니거나 지금 할 일들이 그렇게 많지가 않다면 최대한 열린 자세로 요청을 검토해야겠다. 규약이라는 것이 중요하지만, 그것보다 더 중요한 것은 팀원과 협업을 잘 해나가는 것을 깨달았다.

2. 방어적인 소통 자세

팀원이 내 소통 방식이 조금은 방어적이라고 느꼈다고 한다. 되돌아보았는데, 팀원이 그렇게 느낄 수 있을거라는 생각이 들었다. 변경을 요청한 부분에 대해 좀 더 토론해보고 함께 더 나은 방향으로 나아가려고 했어야 했는데, 나는 rfc 문서와 www 문서를 가져오면서 이런 이유때문에 변경 요청에 응해줄 수 없다는 식으로 딱 잘라서 말했다. 그래서, 팀원이 내가 방어적이고 내 말이 조금 강하게 느껴졌던 것 같다.

앞으로는, 내가 확고한 생각을 가지고 있더라도 열린 마음으로 이야기를 나눠봐야겠다. 내가 아는 사실이 맞다는 생각을 하지말자. 팀원과 이야기하면서 더 나은 방향으로 나아가자.

3. 내가 먼저 한 발자국 물러서면 상대방도 한 발자국 물러선다.

위와 같은 점들을 느끼고 팀원을 대하는 자세를 조금 바꿔보았다. 내가 조금 양보하는 자세로 말하니까 팀원도 양보하는 자세로 말해준다. 물론, 팀원도 나에게 먼저 양보하는 자세로 말했을 것이고 이전부터 그랬을 수도 있다.

서로 양보하는 자세라는 것을 느꼈고, 그렇게 소통하니 합의가 더 잘되고 기분도 좋다.

사람이 인생에서 힘듦을 느끼는 가장 크고 많은 이유는 인간관계라고 생각한다.
결국 개발은 사람이 하는 것이다. 성능, 규약 중요하다. 그런데, 더 중요한 건 함께 개발을 해나가는 팀원과의 관계다.

중간 전시까지 모든 기능을 다 구현하기 때문에 API를 정말 많이 뽑았다. 정말 API 공장 같다. 거기에, 학교 과제들이 정말 쏟아져 나온다..

이렇게 시간에 쫓기기 때문에 많은 고민을 하지 못해서 아쉽다. 어떻게 하면 이런 조건속에서 더 좋은 코드를 작성하고 더 좋은 설계를 할 수 있을까?

아무리 바빠도 포기할 수 없는 것은 Test이다. Test 없이는 이제 개발할 수가 없는 사람이 되었다. 바쁜 와중에 테스트는 무조건 작성하려는 내 모습을 보니 조금은 뿌듯하다..!

아 몰라, 화이팅~!

profile
self-motivation

0개의 댓글