모아모아 3차 데모 후기

디우·2022년 8월 22일
1

모아모아

목록 보기
11/17

우아한테크코스 레벨3, 우리팀(모아모아)의 3차 데모 기간동안 느낀 점 등을 기록하려고 한다.
(3차 데모 기간, 7월 25일 ~ 8월 5일)


진행한 사항

이번 스프린트3에서도 이전 스프린트들과 마찬가지로 굉장히 많은 사항들을 진행하였다.
가장 기억에 남는 진행사항 중에는 처음해보는 HTTPSREST Docs 적용 그리고 로깅스케쥴링 같은 기능들이 있다.
또한 이번 3차 데모데이 발표는 내가 맡게 되었는데 남들 앞서 서서 이야기하는 경험이 적은 나에게는 새로운 경험이었다.
발표와 관련한 나의 자세한 생각이나 감정들은 우아한테크코스에서 매 레벨마다 작성하는 글쓰기 미션에 드러나 있다.
팀 프로젝트가 나에게 남긴 것


태그를 이용한 스터디 조회 리팩토링

이전에 스프린트 2-1 에서 QueryDsl 로 열심히 동적 쿼리를 작성하였다.
동적 쿼리 작성하기

이를 위해서는 다음과 같은 단점들을 안고 가야만 한다.

태그스터디 라는 다대다 관계 사이에서 이를 일대다 그리고 다대일로 풀기 위해서 생기는 스터디_태그 라는 연관관계 테이블의 구조를 객체(Entity)에서도 가져야하게 된다.
하지만 이는 우리가 JPA를 쓰는 이유를 무색하게 만든다.
JPA는 ORM이다. 즉, Object와 Relation(즉, 테이블) 을 매핑해주기 위한 용도이다. 한마디로 객체는 객체대로 설계하고, 테이블은 테이블대로 설계한 후에 이 사이를 매핑하기 위해서 우리는 JPA를 사용하는 것이다. 하지만 테이블 구조(스터디_태그)를 그대로 가져와서 StudyTag 객체를 만드는 것이 과연 적절한가? 하는 생각이 들었다.
우리가 만약 DB와 관계없이 순수한 자바 애플리케이션을 만들었다면 과연 StudyTag 라는 객체가 생겼을까? 하는 의문이다.
나를 포함한 우리팀은 아니오. 라는 대답이 나왔고, DB 구조를 그대로 가져온 객체 설계는 적절하지 않다고 판단하였고, 우리는 StudyTag 라는 객체를 지우고 대신 식별자를 가지는 방향으로 구현하였다.
그렇게 되면 식별자(Long) 타입의 값을 가지고 있는 것은 과연 객체지향 적인 것인가?? 하는 의문이 들 것이다.
우리팀에서는 이를 Study 객체에 AttachedTag 라는 객체를 참조하고 있는 구조로 풀었으며, 이 AttachedTag 안에 Long tagId 를 가지도록 구현하였다.

다음으로는 계속해서 변경되는 패키지 구조가 문제가 되었다.
당시 스프린트 2-1, 즉 이제 막 프로젝트를 시작하고 있는 단계였기 때문에 패키지 구조가 적지 않게 변경되었다.
그럴 때 마다 우리는 QueryDsl 을 사용하고 있기 때문에 Q타입 객체를 위해서 계속해서 빌드를 다시 해주어야 했다. 여기서 팀원들은 번거로움을 느꼈다.

결정적으로는 현재 우아한테크코스 레벨3 에서 QueryDsl 을 금지하고 있었다. 동적쿼리도 충분히 메소드를 활용한 분기문(if문) 을 통해서 구현이 가능하기 때문에 QueryDsl을 사용하지 말라는 것이었다.

결국 우리팀은 위와 같은 여러가지 사항으로 인해서 QueryDsl 에 대한 의존성을 제거하고 순수 SQL 쿼리문으로 변경해서 작성하였다.

StudySummarDao의 동적 쿼리 부분

하지만 여전히 쿼리를 단순 String 으로 작성해야한다는 부분에 대해서는 의문이 든다. 오타가 발생햇을 때, 이를 컴파일 타임에 잡아줄 수 없다. 심지어 런타임 시간에도 숨어있다가 실제 해당 쿼리가 호출되는 시점에 사용자가 예외를 만나면서 우리는 쿼리가 잘못되었다는 것을 알아 차릴 수 있다.
실제로 개발중에도 단순히 스페이스(' ') 공백이 빠져있어서 쿼리가 제대로 동작하지 않는 것인데 이를 제대로 파악하기 어려운 경험을 많이 하였다. 다음과 같이 어떻게 보면 단순한 쿼리에 대해서도 만약 공백이 하나 빠져있다고 하면 여러분들 또한 찾기 어려울 것이다.

        String sql = "SELECT DISTINCT study.id, study.title, study.study_status, study.current_member_count, "
                + "study.max_member_count, study.start_date, study.end_date "
                + "FROM study LEFT JOIN study_member ON study_member.study_id = study.id "
                + "WHERE study_member.member_id = :id OR study.owner_id = :id";

그래서 아직 팀에 적용하기는 어려울 수도 있지만, Querydsl 의 장점을 활용해보면 어떨까? 하는 생각이 든다.
앞서 언급한 Querydsl의 단점, 즉 패키지 구조 변경에 따른 재빌드는 프로젝트 초기였기에 발생한 비용이었다고 생각한다. 현재는 패키지 구조가 안정화되었고, 변경이 빈번하게 발생하지 않고 있기 때문에 앞서 언급한 불편함은 없다고 봐도 무방하다고 생각한다. (설령 해당 비용이 있다고 하더라도, 앞서 보인 것과 같이 문자열로 쿼리를 작성할 때 잘못 작성하여 버그를 찾는 비용이 더 크다고 생각한다.)
Querydsl 을 사용하면 타입 안정성을 제공한다. 즉, select() 나 selectFrom() 과 같이 쿼리를 자바 코드 자체로 작성 가능하므로 잘못된 쿼리 작성시 컴파일 타임에 알아차릴 수 있다. 또한 가독성 측면에서도 문자열로 작성하는 것도 나쁘지는 않지만 이렇게 Querydsl 을 사용할 때 더 좋다는 생각이 든다.

하지만 지금 당장 적용하기 힘들다고 말한 이유는 아직 팀원 모두가 Querydsl 에 대해 익숙하지 않고, 본인 또한 잘 사용하는 것은 아니다. (맛을 보았다 라고 이야기하는 것이 적절한 것 같다.) 왜냐하면 식별자를 사용하게 됨에 따라서 eq() 메소드와 같은 것을 사용할 때 이전에 Q타입 객체가 일치하는지를 판단하는 것이 아니라 이제는 식별자가 같은지를 판단해야한다. 따라서 이를 어떻게 이전(Q타입)과 같이 적절하게 활용할 수 있는지 아직은 잘 모르겠다.

스터디 참여 기능

[BE] issue122: 스터디 참여 기능

스터디 참여 기능 을 구현하였다. 처음으로 다른 팀원들과 함께 하지 않고 혼자서 기능 개발을 했던 시간이었던 것으로 기억된다.
우리팀에서는 스프린트3에 들어오면서 기능 개발에 필요한 순간에만 서로 함께 페어를 하거나 논의를 하는 등의 방법으로 진행하기로 하였다. 왜냐하면 앞선 스프린트1에서의 몹 프로그래밍 그리고 스프린트2에서의 페어 프로그래밍 을 진행하면서 우리팀만의 컨벤셔이 얼추 맞추어져 있다는 생각이 팀원 모두 들었고, 우리가 앞으로 구현해야할 기능에 비해서 스프린트3 이면 벌써 막바지로 접어들고 있는 것이었기 때문이다.
(실제로 매 스프린트마다 HTTPS 적용 등등 필수로 진행해야하는 요구사항도 만족시키려면 이제는 어느정도 혼자서 개발하는 것이 필요하다고 판단하였다.)

스터디 참여 라는 기능을 선택한 이유는 비즈니스 로직이 단순하지 않을 것이라고 기대했기 때문이었다. 하지만 생각보다 더 복잡했던 것 같다.

따라서 도메인에서의 검증 로직이 중요하다고 판단하였다. 스터디 모집 기간이나 스터디 최대인원 여부를 판별하고 나서 스터디 참여가 가능해야 했으며, 스터디 참여 이후의 스터디 참여 인원 증가가 필요하였다. 그렇기 때문에 Controller 에서 부터 아래로 내려오는 식으로 구현하는 것이 아니라 Bottom Up 으로 도메인에서 부터 올라가는 식으로 구현하였다.

구현을 하다 보니 이미 스터디에 가입한 사람인지에 대한 검증도 필요하게 되었고, 스터디 인원이 다 차게 되면 스터디의 상태를 CLOSE 상태로 변경해주는 작업이 필요하게 되었다.

이러한 스터디 검증과 관련된 로직이나 스터디 참여 이후의 상태 변경등의 로직이 커지게 되었고, 어디서 이러한 해당 비즈니스 로직을 처리해야하나 고민이 들었다. Service에서 해야하나? Domain에서 해야하나?

먼저 스터디 참여 가능 여부는 충분히 Service 계층에서 풀어낼 수 있을 것 같다는 생각을 하였다. 왜냐하면 나는 Service 계층을 다음과 같이 정의하고 있기 때문이다.

Spring 장바구니 - 1단계 미션 PR

즉 스터디 참여가 가능한지 불가능한지 판단하는 로직은 스터디 참여 라는 하나의 트랜잭션 단위의 비즈니스 흐름의 한 부분으로 볼 수 있기 때문이다.

하지만 또 다른 측면에서는 스터디 라는 객체에게 나 지금 스터디 참여 가능해? 라고 물어보는 것도 자연스럽다고 판단하였다. 많은 고민을 한 끝에 다음과 같이 Study 도메인에서 participate 메소드를 Service에서 호출하도록 구현하였다.
이렇게 구현함에 따라 Service 계층은 정말 간단해졌다. Member와 Study 를 DB에서 조회해온 이후에 study.participate(member.getId()) 만 수행하면 된다.

(여기서 member 객체 자체를 넘기는 것이 아니라 member의 id만을 넘기는 이유는 각 패키지 간의 의존성을 최소화화 하고 싶었기 때문이다.(Study 와 Member 는 서로 다른 패키지))

    public void participateStudy(final Long githubId, final Long studyId) {
        final Member member = findMemberBy(githubId);
        final Study study = findStudyBy(studyId);

        study.participate(member.getId());
    }
    public void participate(final Long memberId) {
        if (details.isCloseStatus() || period.isCloseEnrollment() || participants.isImpossibleParticipation(memberId)) {
            throw new FailureParticipationException();
        }

        participants.participate(new Participant(memberId));
    }

최종적으로 participate 메소드는 다음과 같이 리팩토링하게 되었습니다.

    public void participate(final Long memberId) {
        if (recruitPlanner.isCloseEnrollment()) {
            throw new FailureParticipationException();
        }

        final Participant participant = new Participant(memberId);
        participants.participate(participant.getMemberId());

        if (isFullOfCapacity()) {
            recruitPlanner.closeRecruiting();
        }
    }

도메인에 스터디 참여 검증참여 이후의 상태 변경 코드를 둔 이유는 테스트와도 연관이 있다.
Service 계층에 스터디 참여가 가능한지 여부를 판단하는 로직을 두면 Service 단을 테스트하여야 한다. 하지만 우리팀에서는 다음과 같은 형태로 테스트를 구성하여 최대한 적은 비용으로 우리가 원하는 피드백을 테스트를 통해서 받기를 원하고 있었다.

그리고 위와 같이 스터디 참여 가능 여부를 도메인에서 검증하게 되면 우리는 Study 단위 테스트 를 통해서 (@RepositoryTest@SpringBootTest 을 통한 빈 의존성 없이) 우리가 구현한 로직에 대한 피드백을 받을 수 있기 때문이다.

다음은 StudyTest 를 통해서 Study 가입에 대한 테스트를 작성한 모습이다.

    @DisplayName("스터디 참여가 가능한 조건(날짜, 가입 가능 수, 가입여부)이면 스터디에 참여할 수 있다.")
    @Test
    public void participate() {
        final Details details = new Details("title", "excerpt", "thumbnail", "OPEN", "description");
        final Member member = new Member(1L, 1L, "username", "image", "profile");
        final Participants participants = Participants.createByMaxSizeAndOwnerId(10, member.getId());

        final LocalDate enrollmentEndDate = LocalDate.now().plusDays(1);
        final LocalDate startDate = LocalDate.now().plusDays(1);
        final LocalDate endDate = LocalDate.now().plusDays(1);

        final Period period = new Period(enrollmentEndDate, startDate, endDate);

        final Study study = new Study(details, participants, period, AttachedTags.empty());

        final Member participant = new Member(2L, 2L, "username", "image", "profile");

        assertThatCode(() -> study.participate(participant.getId())).doesNotThrowAnyException();
    }
class ParticipantsTest {

    private static Set<Participant> sampleParticipants = new HashSet<>();

    @BeforeEach
    void setUp() {
        sampleParticipants = new HashSet<>(
                List.of(new Participant(2L), new Participant(3L))
        );
    }

    @DisplayName("스터디장은 이미 스터디에 참여한 것이므로 검사 시에 예외가 발생한다.")
    @Test
    public void checkParticipatingAboutOwner() {
        final Participants participants = new Participants(3, 10, sampleParticipants, 1L);

        assertThat(participants.isImpossibleParticipation(1L)).isTrue();
    }
       @DisplayName("이미 참여한 회원은 참여 여부 검사 시에 예외가 발생한다.")
    @Test
    public void checkAlreadyParticipating() {
        final Participants participants = new Participants(3, 10, sampleParticipants, 1L);

        assertThat(participants.isImpossibleParticipation(2L)).isTrue();
    }

    @DisplayName("스터디 회원 수가 꽉 차지 않은 경우 정상적으로 가입이 된다.")
    @Test
    public void checkStudyMemberCountLessThanMaxCount() {
        final Participants participants = new Participants(3, 4, sampleParticipants, 1L);

        assertThat(participants.isImpossibleParticipation(4L)).isFalse();
    }

    @DisplayName("스터디 회원 수가 꽉 찬 경우 예외가 발생한다.")
    @Test
    public void checkStudyMemberCount() {
        final Participants participants = new Participants(3, 3, sampleParticipants, 1L);

        assertThat(participants.isImpossibleParticipation(4L)).isTrue();
    }
    
    @DisplayName("스터디 참여 이후에는 현재 스터디원 수가 증가한다.")
    @Test
    public void IncreaseCurrentMemberCount() {
        final Participants participants = new Participants(3, 4, sampleParticipants, 1L);
        final Participant participant = new Participant(4L);

        participants.participate(participant);

        assertThat(participants.getCurrentMemberSize()).isEqualTo(4);
        assertThat(participants.isImpossibleParticipation(4L)).isTrue();
    }
}

HTTPS

처음으로 HTTPS 를 적용해보았다.

위와 같은 필수 요구사항도 존재했지만, 나름대로 HTTPS를 적용해야하는 이유를 고민해보고, 찾아보았다.

HTTP 완벽 가이드라는 책에서는 온라인 쇼핑이나 인터넷 뱅킹과 같은 웹 트랜잭션에서 중요한 일을 하는 경우에 HTTP 보다 안전한 방식의 HTTPS 가 요구된다고 이야기하였다. 하지만 우리의 서비스는 돈과 같이 중요한 비즈니스를 처리하는 웹사이트는 아니다. 그럼에도 불구하고 왜 HTTPS가 필요할까?

HTTPS는 데이터를 암호화해서 전송하는 것 이외에도 데이터 무경성 또한 보장한다. 데이터가 전송되는 중에 의도적으로 또는 비의도적으로 감지되거나 탐지되지 않게끔 하고, 수정 및 손상이 되지 않게 해준다.
또한 우리의 client가 신뢰할 수 있다. 사용자가 접속한 웹사이트가 공격자로부터 보호받고 신뢰할 수 있음을 보장해줄 수 있다.

결론적으로 금전적인 비즈니스와 연관이 없다고 하더라도 우리 서비스의 사용자로 하여금 신뢰를 제공하고, 데이터의 무결성 등 서버와 제대로 통신하고 있따는 것을 보장하기 위해서라도 HTTPS 를 적용해야하는 것이다.

HTTPS 의 동작원리와 HTTPS 도입 과정에 대해서는 다음의 글에서 자세히 정리하였으므로 다음 글을 참고하길 바란다.

모암와 HTTPS 도입기

JPA Auditing 적용

[BE] issue164: 스터디 자동 CLOSE 및 Auditing 포함 전반적인 리팩토링

프로젝트를 진행하면서 날짜 정보에 대한 관리가 필요해졌다.

가장 먼저 스터디 목록을 보여줄 때, 이제는 스터디 생성 날짜 를 기반으로 해서 가장 최근에 만들어진 스터디를 상단에 보여주도록 정렬을 해줄 필요성이 있게 되었다. 또한 리뷰를 작성시에 다음과 같이 리뷰 작성 날짜를 보여줄 수 있어야 했다.

뿐만 아니라 향후에는 스터디 가입날짜도 보여주어야 했다. (스프린트4 때 구현 예정) 그리고 스터디 가입 날짜의 경우 owner 는 스터디 생성 일자가 곧 owner가 스터디에 가입한 날짜가 되게 되고, 스터디원들의 경우 study_member 테이블에 row가 추가되는 날짜가 곧 스터디에 가입한 날짜가 된다. 따라서 study_member 테이블에 created_date 컬럼을 추가하고, 해당 날짜에 대한 관리가 필요해졌다.

더불어 우리가 초기에 밀리초 단위 까지 고려하던 스터디 시작 날짜, 스터디 종료 날짜, 스터디 모집 마감 날짜에 대해서도 이제는 년, 월, 일 만 관리하게 수정해주어야했다.

이는 특히 jdbcTemplate 으로 DB를 초기화해주던 우리의 테스트 코드에서 다음과 같이 의미없은 초 데이터를 넣어야한다는 데에서 특히나 리팩토링 해야할 필요성을 느끼게끔 하였다. (의미 없는 밀리초 정보가 함께 들어가기 때문에 가독성이 떨어지고 쿼리문이 굉장히 길어지게 되었다.)

jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruit_status, study_status, description, max_member_count, created_at, owner_id) VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'CLOSE', 'PREPARE', '디우의 HTTP 정복하기', 5, '2021-11-08T11:58:20.551705', 3)");

나는 스터디 생성 날짜, 리뷰 작성 날짜, 스터디 가입날짜 등에 대해서 애플리케이션 단에서 LocalDate.now() 를 호출해서 저장하는 것이 아니라 자동으로 관리되었으면 했고, 테스트 시에 자동으로 날짜 정보를 포함하였으면 하여 JPA Auditing 을 고려하였다.

가장 먼저 JpaAuditingConfig 파일을 작성하였다.

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}

기존에는 MoamoaAppilcation.java@EnableJpaAuditing 만 추가해주었는데, 그린론 피드백을 통해서 기존의 @SpringBootApplication 과 함께 있던 config를 분리해주게되었는데, 이는 @WebMvcTest 를 사용해서 테스트를 하면 JPA와 관련된 빈을 등록하지 않기 때문에 @MockBean 을 이용한 테스트 코드에 JpaMetamodelMappingContext.class 를 매번 추가해줘야한다는 단점을 안고 가야하기 때문이었다.

@EnableJpaAuditing 어노테이션은 말 그대로 Jpa Auditing 기능을 사용하겠다는 의미이다.

이후 BaseEntity 라는 도메인을 commom.entity 패키지에 추가해주었다.

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false, nullable = false)
    private Date createdDate;

    @LastModifiedDate
    @Column(nullable = false)
    private Date lastModifiedDate;
}

만든 날짜와 함께 가장 최근에 수정된 날짜를 포함하도록 구성하였다. (이후에는 Date 타입에서 LocalDateTime 으로 변경해주었다. Date 는 불변이 아닌 것 등등 여러가지 이슈로 사용을 권장하지 않는다. 참고 자료)
(여기서 createdDate 는 수정이 불가하게, 또 생성되면 무조건 해당 필드가 추가도리 것이므로 nullable을 false로 지정해주었다. lastModifiedDate 에 대해서도 처음에 추가되면 createdDate와 같은 날짜로 들어가게 되므로 nullable을 false로 지정해주었다.)

여기서 MappedSuperclass 어노테이션은 해당 클래스를 상속받는 클래스에 JPA 속성을 물려주겠다라는 의미이다. 즉 공통 매핑 정보를 묶어놓은 것이라고 보면 되고, 이를 상속해서 구현하는 클래스들은 해당 공통 매핑 정보를 포함하게 된다.

이렇게 해서 Review 도메인이나 Study 와 같이 날짜 정보가 필요한 도메인에 대해서 BaseEntity를 상속 받도록 하였다. 예시로 Review 도메인은 다음과 같이 수정되었다.

위와 같이 수정해준 덕에 테스트 코드에서도 다음과 같이 이전보다는 훨씬 깔끔해질 수 있게 되었다.

        jdbcTemplate.update("INSERT INTO study(id, title, excerpt, thumbnail, recruitment_status, study_status, description, max_member_count, created_date, last_modified_date, owner_id) "
                + "VALUES (4, 'HTTP 스터디', 'HTTP 설명', 'http thumbnail', 'RECRUITMENT_END', 'PREPARE', '디우의 HTTP 정복하기', 5, '" + now + "', '" + now + "', 3)");

하지만 이는 JPA를 통해서 DB에 데이터를 저장하는 것이 아니므로 Auditing 기능을 활용할 수 없어 여전히 날짜 정보를 직접 삽입(insert) 해주는 모습이다. 하지만 이전과 달리 final LocalDateTime now = LocalDateTime.now(); 를 활용하기 때문에 보다 간견해지고 직관적으로 수정된 모습이다.

또한 Study 에 대해서는 Auditing을 제거하고 다음과 같이 직접 createdAt 값을 관리하도록 하고 있다.

@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Study {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Embedded
    private Content content;

    @Embedded
    private Participants participants;

    @Embedded
    private RecruitPlanner recruitPlanner;

    @Embedded
    private StudyPlanner studyPlanner;

    @Embedded
    private AttachedTags attachedTags;

    @Column(updatable = false)
    private LocalDateTime createdAt;
    
    ...
}

그 이유는 스터디 생성 날짜, 리뷰 작성 날짜, 스터디 가입날짜 에 대해서 불변식을 검증할 때 Study 객체가 생성되는 현재 날짜 정보가 필요하기 때문이다.

REST Docs

기존에 Github Wiki 에서 관리하던 API 명세를 REST Docs로 변경하게 되었다.
스프린트 1,2 때에는 API 가 많지도 않았고, 기존 API 명세가 변경되는 일도 잦지 않았다.
하지만 스프린트3에 접어들면서 기능구현도 많아지고, 기존의 API에 새롭게 필드가 추가되거나, 날짜 형식이 변경되는 등 사소한 변경이 자주 발생하였다. 하지만 이렇게 변경이 될 때 마다 API 명세를 업데이트하지 않아 Front와 Back 사이에 API가 서로 다르게 구현하는 사태도 발생하였다.

따라서 우리는 API 문서 자동화를 고려하엿고, 짱구 가 REST Docs 설정을 진행해주었다.

[BE] issue185: REST Docs 설정

이 때, 짱구는 DocumenttationTest 라고 하는 별도의 테스트를 통해서 문서가 뽑히게끔 구현하였다.
하지만 팀원들과 다시 이야기한 결과, 우리가 수행하는 인수테스트나 Controller, Domain 테스트와는 무관한 DocumentationTest 를 통해 문서화를 하는 것은 관리 포인트가 2개가 되는 것이고 Spring Rest Docs의 장점인 테스트를 통한 문서화 작성 그리고 테스트가 통과해야만 문서가 뽑힌다. 라는 장점이 무색해지는 것 아닌가 하는 의견이 나오게 되었고, 정상 시나리오를 다루는 인수테스트를 통해서 API 문서를 도출하도록 수정을 진행하게 되었다.
(예를 들어 DocumentationTest 는 통과하여 문서가 뽑히는데, 인수테스트는 통과하지 않을 수 있거나 테스트 자체가 잘못되어 통과될 수도 있다.)

결론적으로 MockMvc에서 RestAssured를 활용하여 REST Docs 문서를 뽑도록 수정을 진행하였다.

[BE] issue203: rest docs 설정 변경

로깅

이전에 정리하였던 우린 어떤 로그를 써야할까? 와 이어서 우리는 팀프로젝트에 로깅 전략을 도입하였다.

이전에 위의 글에서 언급했던 것과 같이 로깅 벤치마크를 근거로해서 우리는 Logback 을 활용하였다. Logback은 log4j 에 비해 향상된 필터링 정책이나 기능, 로그 레벨 변경 등에 대해 서버를 재시작할 필요없이 자동 리로딩을 지원한다는 장점이 있기 때문이다. 또한 스프링 부트에서 제공하는 로깅 프레임워크에 Logback 도 포함되어 있기 때문이다.

우리는 Dev 서버에 대해서는 Debug 모드로 설정해주어 SQL 쿼리까지 확인할 수 있도록 구성해주었다.
또한 Prod 서버에 대해서는 이전에 언급한 것과 같이 Info 로 설정하여 운영에 참고할만한 사항이나 중요한 비즈니스 프로세스가 완료되었는지 여부만 확인할 수 있도록해주었고, log.debug("data={}", data) 와 같이 + 연산이 아닌 올바르게 로그를 찍을 수 있도록 구성해주었다.

springframework 와 woowacourse.moamoa (우리 어플리케이션) 모두 INFO 로 로그를 찍도록 설정해주었다.

springframework.web 은 DEBUG로 springframework는 INFO 로 해주었고, 우리의 애플리케이션과 jdbc 쪽 모두 DEBUG로 설정해주었다.

우리는 prod 와 dev 모두 7일 그리고 100MB 로 파일의 최대 저장 기한과 용량을 설정해주었다.

스케쥴링 (스터디 진행 상태 자동 변경)

스터디 진행 상태가 Optional한 값인 endDate 가 있는 스터디인 경우에는 스터디의 진행 상태가 자동으로 종료되도록 해주어야한다. 따라서 우리는 Spring Scheduler 기능을 활용하게 되었다.

Spring Scheduler Discussion

참고한 자료들은 Spring Batch와 Scheduler, Spring 스케쥴러(자동배치) 설정 이다.

[BE] issue188: 스터디 상태 자동 변경

로직을 어떻게 구현하였는지 간단하게 설명해보면 다음과 같다.
가장 먼저 Study 테이블에서 OPEN 인 스터디들을 모두 조회해 온 이후에 그 중에서 마감날짜(enrollment_end_date) 가 오늘의 날짜와 같거나 넘긴 스터디들을 찾는다.
찾은 날짜들에 대해서 CLOSE 상태로 바꿔는 update 쿼리를 수행하는 간단한 작업이다.

해당 작업에 대해서 StudyService 코드는 다음과 같이 작성하였다.

    public void autoUpdateStatus() {
        final List<Study> studies = studyRepository.findAll();
        final LocalDate now = dateTimeSystem.now().toLocalDate();

        for (Study study : studies) {
            study.changeStatus(now);
        }
    }

changeStatus() 메소드는 study 진행 상태와 더불어 모집 마감 날짜도 동일하게 오늘과 같거나 넘긴 날짜에 대해서 모집 마감 상태로 변경해주어야하기 때문에 다음과 같이 구현되어 있다.

    public void changeStatus(final LocalDate now) {
        recruitPlanner.updateRecruiting(now);
        studyPlanner.updateStatus(now);
    }

그리고 해당 메소드와 메소드를 포함한 Service를 아래와 같이 TriggerTask 상속받는 AutoCloseEnrollmentTask 에 설정하고 이를 빈으로 등록해줌으로써 원하는 시간에 동작하게 된다.

runnable 메소드에서 매개변수를 통해 studyService를 받도록 하고 여기서 autoUpdateStatus() 메소드를 호출하도록 한다. 그리고 이를 생성자에서 new CronTrigger() 와 함께 등록해준다. Asia/Seoul 을 기준으로 @daily 즉, 매일 00시 마다 생성자로 함께 준 runnable 메소드를 수행해준다는 것이다.

@Component
@Slf4j
public class AutoCloseEnrollmentTask extends TriggerTask {

    public AutoCloseEnrollmentTask(final StudyService studyService) {
        super(runnable(studyService), new CronTrigger("@daily", ZoneId.of("Asia/Seoul")));
    }

    private static Runnable runnable(final StudyService studyService) {
        return () -> {
            log.debug("{} : start moamoa scheduled task!", LocalDateTime.now());
            studyService.autoUpdateStatus();
        };
    }
}

이전에 구현하였던 모집 마감과 함께 하나의 메소드(autoUpdateStatus) 로 해준 이유는 두 메소드를 서로 독립적으로 실행하도록 하였을 때 서로 다른 값이 overwrite 될 수 있기 때문이다. 즉, 어떤 A라는 스터디에 대해서 모집 마감 자동 업데이트는 스터디 진행상태: OPEN, 모집: CLOSE 로 업데이트하는데, 동일한 A가 스터디 상태 자동 변경 업데이트에 대해서는 스터디 진행상태: CLOSE, 모집: OPEN 이 되며 이 둘은 독립적으로 수행되고 어떤 메소드가 먼저 동작할지 모르므로 DB에는 둘 중 하나의 상태로 저장되게 된다. (불변식은 무시한 예시이다.)
하지만 우리가 원하는 결과는 스터디 진행상태: CLOSE, 모집: CLOSE 이다. 따라서 이 두가지에 대해서 함께 로직을 수행하고 DB에 업데이트 되도록 구성한 것이다.

위와 같이 간단하게 스케쥴러를 등록해 원하는 대로 동작하도록 구성하였지만, 실제로 우리가 원하는 대로 동작하는지 테스트가 필요하였다.

가장 먼저 우리가 등록한 Task가 매일 00시 00분에 실행되는지 확인하는 테스트를 작성하였다.
현재 시간과 다음에 시작될 것으로 예상되는 시간들을 통해서 이를 확인하였다.

또한 추가적으로, dev 서버에 배포하여 우리가 원하는대로 00시에 동작을 하는지 로그를 통해 확인도 해보았다.

생각보다 Spring Scheduler 사용이 어렵지 않았지만, 이를 어떻게 테스트할 것인가에 대해서는 고민이 많이 드는 작업이었다.

데모 발표

이번 3차 데모데이 발표는 내가 맡아서 진행하였다.

(곧 Youtube에 영상이 올라오면 링크를 첨부하도록 하겠습니다.)

정말 굉장히 많이 떨었다. 전날 까지는 크게 아무 느낌이 없었던 것 같다. 떨리기 보다는 부담감이 들었다. 우리팀이 열심히 작업한 내용을 내가 모두 전하지 못하면 어떡하지? 하는 걱정이 들었다. 그래서 PPT 도 다시보기를 반복하고 발표 연습도 여러번 진행하였다. 그런데 막상 당일날이 되니 부담감도 부담감이지만 떨리는 마음이 굉장히 커졌다.
얼마나 떨었으면 팀원들이 왜이렇게 떠냐고 마음 가볍게 먹고 나가서 이야기하고 오라고 이야기해주기 까지 했다. 심지어는 앞에 나가서 소리라도 한 번 지르면 편할거라고 조언해주기도 했다.
하지만 긴장되는 마음이 없어지지는 않았다..

그리고 막상 발표를 시작하고 나니 머리가 하얘지면서 앞을 못보고 계속 PPT 화면만 보면서 이야기하게 되었다. 그리고 대본없이 발표를 진행하는 거이다보니 연습 때 이야기하였던 내용을 빠트리고 이야기하기도 하였다. 지금 와서 돌이켜 생각해보면 포비를 포함해서 코치님들이 많이 있었고, 이전 팀들의 발표에서 코치님들의 날카로운 질문들이 쏟아져, 살짝 움츠려든 채로 발표를 진행하였고, 괜히 '어 이거 틀린거 아닌가?' 하며 스스로 애매하다고 생각되는 내용을 생략하며 발표를 진행하였던 것 같다.

발표가 끝나고 짱구에게 어땠냐고 물어보았다. 짱구는 솔직하게 피드백 주었다. 아무래도 내가 많이 떨다보니 '떨고 있구나' 하는 것이 안느껴지지는 않았지만, 또 계속해서 떤 것도 아니었다고 이야기해주었으며, 디우가 말하고 싶은 내용이 많아서 아쉬움이 남는 것이다. 정말 괜찮은 발표였다고 이야기해주었다.

이렇게 발표가 끝나고 나니 새롭게 느껴지는 부분이 많았다. 학창시절을 포함해서 단 한 번도 남들 앞에서 발표를 해본 적이 없던 것 같다. 팀플을 해도 발표는 내가 맡아서 하지 않았으며, 코로나 이후로는 온라인으로 수업이 진행되었기 때문에 발표 영상을 녹화해서 제출하는 식으로 발표를 진행했지 남들 앞에 서서 발표를 한 적은 없었다. 대본없이는 더더욱.. 하지만 이렇게 발표를 해보고 나니 생각보다 남들 앞에 서서 이야기하는 것이 그렇게 어려운 것은 아니구나 생각이 들었고, 나도 할 수 있구나 하는 자신감도 얻은 것 같다. 그리고 이와는 별개로 다시 하면 잘할 수 있었을 텐데 하는 아쉬운 감정도 들었다.


아쉬운 점

앞서 언급한 것과 같이 발표가 가장 아쉬운 것 같다. 너무 많이 떨었고, 전하고 싶은 말들을 모두 전하지 못한 것 같다. 너무 움츠려들었다. 다음에 만약 이렇게 남들 앞에서 이야기하는 기회가 주어진다면 자신감 있게 그리고 지금보다는 더 나은 발표를 할 수 있을 것 같다.
그리고 기술적인 내용을 이야기할 때, 스스로 '이거 틀린 내용이면 어떡하지?' 하는 걱정이 있는 것 같다. 그런데 틀린면 어떤가? 틀리고 한 번 크게 부끄러운 감정을 느끼고 새롭게 배우면 이는 더 오래 기억되고 머릿속에 남을 것이다. 그리고 이렇게 성장해 나가는 것이라는 생각이든다.


잘한 점

아무래도 데모데이 발표를 맡아서 진행해야해서 그런지 이전보다도 더 다른 팀원들이 진행한 내용도 모두 숙지하려고 노력하였던 것 같다. 로그 부분이나 스케쥴러(물론 스터디 상태 변경과 같이 추가적인 스케쥴러 작업을 진행하기는 했지만..), REST DOCS 까지 내가 맡지 않은 부분도 내가 조금씩해보면서 (ex. REST DOCS RestAssured로의 전환) 모두 숙지하려고 노력하였다. 그리고 그런 와중에 기능 구현에도 소홀하지 않았고, 미리미리 PPT도 준비하였다. 이러한 점에서 이번 스프린트3은 정말 보람차고 열심히 보냈던 것 같다.

profile
꾸준함에서 의미를 찾자!

0개의 댓글