[Kakao Cloud School] 22번째 회고록

lango·2023년 4월 3일
0
post-thumbnail

Intro


동료와 함께 성장하기

프로젝트를 진행하면서 팀원들과 협업을 하면서 느꼈던 것들 중 하나는 함께 성장하기가 어렵다는 것이다.

팀 프로젝트 가운데 대부분 각자 주어진 몫을 해내기 위해 노력하지만 팀과 팀원 모두의 몫까지 신경쓰면서 개발하기가 생각보다 쉽지 않았다. 개발사항을 PR로써 공유하고 코드 리뷰를 하긴 하지만 단지 main 브랜치로 Merge하기 위한 수단일 뿐이었다.

그래서 팀원들에게 프로젝트 개발사항만을 가지고 소통하는 것이 아니라 직접 읽었던 좋은 개발 관련 뉴스나 기사를 공유하는 것부터 시작하여 우리가 도입하고 사용하는 여러 기술들의 목적을 고민하고 공유해보기로 하였다.

당장 코드리뷰를 신속하게 진행하고 프로젝트 개발 진도를 빼는 것도 중요하지만, 프로젝트 진행 과정 안에서 왜? 이 기술을 도입했지, 어떻게? 동작하는 것이지 라는 고민이 점점 줄어들었다는 것이 가장 큰 이유이다.

그래서 프로젝트 개발과 관련된 부분에서 벗어나 좀 더 본질적인 부분에 대해서도 고민하며, 보다 폭넓은 관점에서 프로젝트의 목적이나 방향성에 대해서 바라보고 논의하는 시간들이 필요하다고 느꼈고, 나와 팀원의 성장에 도움이 되는 과정이 될 것이라고 생각하여 이같은 스프린트나 미팅 시간을 늘리기로 하였다.




Week 22

카카오 클라우드 스쿨 22주차 101~105일까지의 공부하고 고민했던 흔적들을 기록하였습니다.

Github를 협업하기 좋은 환경으로 개선하기

여러 애플리케이션의 관리를 온전히 Github에서 하다보니 혼자만의 저장소보다는 팀 단위의 협업 저장소를 많이 이용하게 되는데 저장소를 혼자 관리하는 것 보다 팀 단위 저장소를 관리하는 것이 훨씬 신경쓸 것이 많았다.

그래서 이번에는 Github에서 협업할 때 개발 생산성을 높이기 위해 여러가지 설정을 진행해보려 한다.

GitHub에서 팀원들의 Approve를 받아야 PR의 Merge가 가능하도록 설정하기

위와 같이 Merge 정책이 별도로 설정되어 있지 않다면 PR 승인을 받지 않아도 업스트림 저장소에 Merge할 수 있다.

그런데, 이는 누구나 Merge 권한이 있기에 리뷰의 중요성이 그리 크지 않다. 그래서 팀원들의 리뷰와 승인이 있을 때에만 Merge를 진행할 수 있는 제약 조건이 있다면 보다 책임감을 가지고 서로의 코드 리뷰에 신경을 쓰지 않을까 하는 목적으로 Merge 정책을 변경하기로 하였다.

GitHub에서 팀원들의 승인이 2개 이상일 때에만 Merge 가능하게 제약을 거는 방법에 대해 알아보자.

Merge 정책을 설정할 Repository의 Setting으로 이동하여 Branches 메뉴로 이동하자.

Add branch protection rule라는 버튼이 있는데 이는 말 그대로 브랜치를 보호하는 규칙을 설정하는 역할을 한다. 해당 버튼을 선택하자.

여기서 Branch name pattern은 반드시 입력해야 하는데 직접 입력한 규칙이 적용될 브랜치의 이름을 적어야 한다.

하지만, 우리는 해당 저장소에서 개발을 하며 수많은 브랜치를 만들어야 하기 때문에 만들게 될 모든 브랜치의 이름을 다 작성할 수는 없다.

그렇다면 *는 모든 것을 뜻하기 때문에 가운데 /가 들어가는 모든 브랜치라는 뜻으로 **/** 형식으로 작성하였다.

적용된 결과를 보면 main 브랜치에도 만든 브랜치 보호 규칙이 적용됨을 알 수 있다.

이제 PR의 Approve 개수를 지정해야 한다.

approve의 개수는 기본 값으로 1명으로 설정이 되어있는데, 팀원들 중 2명 이상이 approve 해야만 하도록 2명으로 설정하였다.

그렇다면 해당 저장소에는 올라가는 PR마다 2명 이상이 승인해주어야 Merge할 수 있게 된다.


Github에서 PR시 자동으로 Reviewer 할당하기

위와 같이 PR을 올리다보면 매번 리뷰어를 할당하기가 은근 귀찮다. 그래서 PR을 생성할 때 리뷰어를 자동으로 할당해주는 스크립트를 작성해보려 한다.

어떻게 리뷰어를 자동으로 할당해줄 수 있을까? Github에서는 PR을 생성할 때 자동으로 리뷰어를 지정해주는 기능이 있다. 바로 CODEOWNERS이다.

CODEOWNERS의 사용법은 매우 간단하다.

CODEOWNERS이라는 파일을 rootdocs/, .github/ 디렉토리 중 원하는 디렉토리에 위치시키면 된다.

CODEOWNERS와 관련하여 Gitgub 공식 문서에는 다음과 같이 설명하고 있다.

To use a CODEOWNERS file, 
create a new file called CODEOWNERS in the root, docs/, or .github/ directory of the repository, 
in the branch where you'd like to add the code owners.

그래서 CODEOWNERS 파일을 아래와 같이 작성하였다.

*.java @Jooney-95 @kcs-developers/start-dream-team

내용을 살펴보자면, @Jooney-95, @kcs-developers/start-dream-team 이라는 사용자가 Java 관련 확장자를 가진 모든 파일들의 권한을 담당하게 된다는 의미이다.

여기서 @kcs-developers/start-dream-team와 같이 리뷰어로 팀을 지정하였다.

해당 저장소의 권한에 개설한 팀을 추가해야 리뷰어 할당이 가능하다는 것을 명심하자.

그렇게 .github 디렉토리에 CODEOWNERS 파일을 추가하였다. 저장소에서 정상적으로 해당 파일을 인식하고 있음을 확인할 수 있다.

이제 실제로 PR을 올렸을 때 리뷰어 할당이 자동으로 되는지 확인하자.

정상적으로 start-dream-team 팀에게 리뷰어 할당이 된 것을 볼 수 있다.

CODEOWNERS 파일에는 @Jooney-95 도 지정했지만 해당 사용자가 본인이라 본인은 리뷰어로 할당이 불가한 것으로 보인다.



Spring Boot에서 Entity간의 1:1관계는 어떻게 지정해야 할까?

프로젝트에 사용자와 사용자의 점수의 관계를 설정하는 과정에서 발생한 문제와 해결했던 방법들을 기록하려 한다.


요구사항 살펴보기
사용자마다 점수가 부여되며 이 점수는 사용자별로 획득 및 차감될 수 있다. 이 때, 사용자 별로 최신의 누적된 점수를 가지고 있어야 한다. 즉 사용자와 사용자 점수는 1대1 관계로 매핑되어야 한다.

사용자와 점수에 데이터 정규화를 반영하여 분리하기

처음에는 사용자 테이블에 점수를 컬럼(속성)으로 사용하도록 설정할까 고민도 되었지만 중복 로딩을 줄이기 위해 데이터 정규화를 하기로 결정하였다.

데이터 정규화는 데이터베이스의 설계 과정에서 중복성과 종속성을 제거하여 데이터를 효율적으로 저장하는 방법이다. 데이터 정규화를 통해 데이터의 일관성과 무결성을 유지하면서 데이터베이스의 성능을 향상시킬 수 있다.

데이터 정규화를 통한 이점은?

사용자를 Member, 사용자의 점수를 Point로 분리하는 것에 이점이 무엇인지 살펴보자.

먼저 불필요한 데이터 로딩을 줄일 수 있다.
Member와 Point를 함께 조회하면 Member 정보만 필요한 상황에서도 Point 정보까지 함께 로딩되어 불필요한 DB 트래픽이 추가로 발생될 수 있다. 이 때, Member와 Point를 분리한다면 사용자의 점수는 필요한 시점에 로딩할 수 있어 서버의 트래픽 및 부하를 줄일 수 있다.

둘째로 데이터 무결성을 보장할 수 있다.
Member와 Point를 하나의 엔티티에서 관리한다고 해보자. Member 정보 변경시 Point 정보도 함께 변경되어야 한다. 이 때, Member 정보만 변경되고 Point 정보는 변경되지 않는 경우, 또는 그 반대의 경우가 발생할 가능성이 있다. 그렇다면 데이터의 일관성과 무결성이 깨지게 되고 결국, 데이터 무결성을 지키지 못하게 된다. 그런데 Member와 Point를 분리하면 두 엔티티를 모두 독립적으로 변경할 수 있기에 데이터 무결성을 보장할 수 있습니다.

마지막으로 코드 유지보수성을 높일 수 있다.
Spring boot를 기준으로 바라보면 Member와 Point를 분리하게 될 경우, 엔티티 클래스를 분리할 수 있어 코드 구조가 보다 깔끔해진다. 또한, 엔티티 클래스가 작아지면서 코드 유지보수성도 높아지게 된다.

외래 키 설정여부는?

1:N 관계에서는 외래 키 여부가 보통 일반적으로 N(다)쪽에서 외래 키를 가지고 있도록 정해져있지만, Member와 Point와 같은 상황에서는 서로 1:1 관계가 되기 때문에 주 테이블이나 대상 테이블 모두에 외래 키를 설정할 수 있다.

그래서 Member에 외래 키를 둘지, Point에 외래 키를 둘지 선택해야 한다.

대상 테이블에 외래 키를 설정!

Member에 Point의 외래키를 설정해둘 수도 있겠지만, 필자는 Point에 Member의 외래키를 설정하도록 하였다.

  1. 마이페이지에서 사용자 정보와 함께 점수를 출력할 경우
  2. 본인의 점수를 클릭하여 점수 획득 내역을 출력할 경우
  3. 타 서비스에서 점수 증가 및 감소 요청이 왔을 경우

위 3가지 경우에서 Member와 Point의 관계가 필요하게 되는데

사실 1,2번의 경우를 보면, Member만을 조회하여 Point를 함께 가져오는 것이 효율적이기에 주 테이블인 Member에 외래키를 두는 것이 좋을 수 있다. 하지만 여기서 고민했던 것은 과연 마이페이지로의 접속이 점수 증감 요청보다 많을까? 라는 점이다.

생각해보니 타 서비스에서의 서비스 이용을 통한 점수의 증감 요청이 마이페이지에서의 트래픽보다 많을 것으로 판단을 내렸고 주 테이블 Member가 아닌 Point에 외래키를 설정하기로 하였다.


엔티티 클래스 분리하고 1:1(일대일) 관계 설정하기

정해진 요구사항대로 Member와 Point를 분리하여 각각의 엔티티를 작성해보았다.

Member.java

@ToString(exclude = {"point"})
@Table(name = "member")
@Entity
public class Member extends BaseTimeEntity {
	...
	@OneToOne(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private Point point;
    ...
}

Point.java

@ToString(exclude = {"member"})
@Table(name = "point")
@Entity

public class Point extends BaseTimeEntity {
	...
	@OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
    ...
}

여기서는 Member와 Point 엔티티의 양방향 연관관계를 지연로딩으로 설정하였다.

Member 엔티티에서는 @OneToOne 어노테이션으로 Point 엔티티와 1:1 관계를 맺고있다고 명시해주었다. 이 때, mappedBy 옵션으로 Point 엔티티의 member라는 필드를 매핑하도록 하였다. 그리고 cascade 옵션을 두어 Member가 삽입되거나 삭제될 때 연관된 Point도 함께 삽입되거나 삭제될 수 있도록 하였다. 마지막으로 orphanRemoval 옵션을 통해 Point 엔티티와 연관된 Member 엔티티가 없다면 Point를 삭제하도록 설정하였다.

또한, Point 엔티티에서는 Member 엔티티의 PK(기본 키)인 member_id 컬럼(memberId 속성)을 @JoinColumn 어노테이션을 통해 연결하였다.


빌더 패턴을 활용한 생성자를 통해 사용자의 점수 부여하기

Member의 point 속성에 casecade 옵션을 부여했으니 Member가 삽입될 때 Point도 함께 삽입되도록 해야한다.

Member가 저장된 이후에만 Point를 저장할 수 있기에 굳이 생성자에 member 필드를 넣어줄 필요는 없지만 Member 엔티티에서 Point를 생성하고 매핑하여 함께 저장할 수 있도록 하기 위해 Point 엔티티 생성자에 member 필드를 추가로 넣어주었다.

Point.java

public class Point extends BaseTimeEntity {
	@Builder
    public Point(Member member, Long point) {
        this.member = member;
        this.point = point;
    }
}

그리고 Member 엔티티의 생성자에서는 Point가 함께 저장될 수 있도록 Point 객체를 만들어 기본값을 부여해두었다.

Member.java

public class Member extends BaseTimeEntity {
	...
    @Builder
    public Member(..., Long point) {
        ...
        this.point = Point.builder().member(this).point(point).build();
    }
    ...
}

테스트 진행하기

MemberRepositoryTest.java

@DisplayName("한명의 회원 데이터 저장")
    @Test
    public void save() {
        // given
        Member member = Member.builder()
                .email("@kakao.com")
                .password("kakao123")
                .nickname("lango")
                .type(Type.LOCAL)
                .role(Role.USER)
                .profileImageUrl("/root/1")
                .isMentor(false)
                .address("서울특별시 강남구")
                .introduce("안녕하세요 저는 ...")
                .point(100L)
                .build();
        memberRepository.save(member);

        // when
        List<Member> list = memberRepository.findAll();

        // then
        Member result = list.get(0);
        Assertions.assertEquals("lango@kakao.com", result.getEmail());
        Assertions.assertEquals("안녕하세요 저는 ...", result.getIntroduce());
        Assertions.assertEquals("lango", result.getNickname());
    }

하나의 Member 데이터를 삽입하는 테스트코드를 작성해보았다.

Member 객체만 만들어서 savea 메소드로 저장하는 구문만 존재하고, pointRepository를 주입하여 Point를 저장하는 구문이 존재하지 않는다.

그런데도 앞에서 설정한 내용으로 인해 Member와 연관된 데이터로 Point 데이터가 잘 저장되는 것을 확인할 수 있었다.




Final..

어느덧 파이널 프로젝트 발표가 한달 정도 남았다.

아직 프로젝트 개발 일정은 많이 쌓여있고, 할 일은 많다. 그리고 일정도 조금씩 밀리고 있다.

그럼에도 불구하고 정말 꼭 필요한 것이 무엇인지 놓치고 싶지 않아서 아무 생각 없이 개발만 하는 것이 아니라 개발하면서 관련된 CS 지식을 함께 공부하는 시간을 가지려고 노력하고 있다.

이번 주까지는 서버단 개발 일정을 타이트하게 가져가고 있는 상황이고 다음주 내로 서버단 통합 테스트를 마치고 클라이언트 애플리케이션 개발을 시작할 예정이다.

일단 클라이언트단 까지 개발이 시작되면 MVP 구현을 목표로 핵심 기능이 동작하는 수준의 프로토타입을 빠르게 개발해낼 계획이다.

혹여 잘못된 내용이 있다면 지적해주시면 정정하도록 하겠습니다.

참고자료 출처

profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

0개의 댓글