Skip to content

namgigun/OurLog

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

✈️ OurLog

🥇 프로그래머스 백엔드 데브코스 3차 실전 프로젝트 13개의 팀 중 1등 (최우수팀 선정)
OurLog는 영화, 도서, 음악 등 다양한 콘텐츠에 대한 감상 경험을 기록하고
이를 다른 사용자들과 공유하며 소통 할 수 있는 웹 서비스입니다.

image


🔗 팀 레포지토리


👥 팀원 소개

이름 프로필 역할
김주은 @jueunk617 • 감상일기 등록/수정/삭제
• 영화·음악 콘텐츠 조회
• 배포 인프라 구축
남기은 (팀장) @namgigun • 감상일기 조회
• 댓글 CRUD
• 도서 콘텐츠 조회
노희철 @Nohheechul • 통계 카드 및 그래프 구현
박영빈 @yeongbin1999 • 회원가입/ 로그인
• 프로필 조회
• 무중단 CI/CD
임종현 @dlawhd • 타임라인
• 팔로우 / 언팔로우
• 좋아요 기능

🧩 주요 기능

기능 설명
프로필 사용자의 감상일기 및 활동 내역 조회
감상일기 콘텐츠 감상 기록 등록·수정·삭제
소셜 팔로우, 좋아요, 댓글을 통한 사용자 간 상호작용
통계 감상 데이터를 기반으로 한 개인화된 통계 제공

🛠 기술 스택

구분 사용 기술
Frontend HTML5 CSS JavaScript TypeScript Next.js
Backend Kotlin Spring Boot Spring Data JPA QueryDSL MySQL Redis
Infra Vercel(front) AWS(back) NGINX Docker
DevOps Github Actions Terraform
Version Control Git Github
Collaboration Notion Slack
Performance Test Apache JMeter
Open API TMDB Spotify 국립중앙도서관

📐 ERD

image

🏗 시스템 아키텍처

image

🙋‍♂️ 담당 역할 및 기여

댓글 기능 Kotlin 마이그레이션 (Java → Kotlin)

수행의도

  • 기존 Java 기반 댓글 서비스는

    • Optional.orElseThrow 중심의 런타임 Null 검증
    • Repository 조회 + 예외 처리 로직의 반복으로 가독성과 유지보수성 저하
  • Kotlin 도입을 통해 컴파일 타임 Null 안정성 확보 및 보일러플레이트 제거를 목표로 함

마이그레이션 전략

변환 순서

의존성이 낮은 계층부터 순차적으로 변환하여, 상위 계층에서 발생할 수 있는 컴파일 오류 최소화

EntityDTORepositoryServiceControllerTest


변환 과정

  1. IntelliJ의 Convert Java File to Kotlin File 기능을 활용하여 1차 자동 변환
  2. 자동 변환 이후, 아래 리팩토링 전략을 기준으로 수동 리팩토링을 진행

리팩토링 전략

1. Null 안전성

  • Spring Data JPA에서 Kotlin 확장 함수인 findByIdOrNull 을 사용하여, Optional 반환 대신 nullable 객체를 반환하도록 변경
  • Java의 Optional 기반 예외처리 방식 → Kotlin의 엘비스 연산자(?:)를 활용하여 컴파일 타임 Null 안정성을 확보

변경 전 (Java)

Diary diary = diaryRepository.findById(diaryId)
                .orElseThrow(
	                () -> new CustomException(ErrorCode.DIARY_NOT_FOUND)
                );

변경 전 (Kotlin)

val diary = diaryRepository.findByIdOrNull(diaryId)
            ?: throw CustomException(ErrorCode.DIARY_NOT_FOUND)

2. 확장 함수를 활용한 보일러 플레이트 코드 제거

  • findByIdOrNull 이후 반복적으로 발생하는 엘비스 연산자 기반 예외처리는 Service 계층 전반에 보일러 플레이트 코드 유발
  • 이를 해결하기 위해, findByIdOrNull + 예외 처리를 하나로 묶은 사용자 정의 확장 함수를 도입

확장 함수 정의

/* extensions.kts 안에 사용자 정의 확장 함수를 생성 */
package com.back.ourlog.global.common.extension

fun <T, ID : Any> CrudRepository<T, ID>.findByIdOrThrow(
        id: ID,
        errorCode: ErrorCode
): T = this.findByIdOrNull(id) ?: throw CustomException(errorCode)

변경 전

val diary = diaryRepository.findByIdOrNull(diaryId) ?: throw CustomException(ErrorCode.DIARY_NOT_FOUND)

변경 후

val diary = diaryRepository.findByIdOrThrow(diaryId, ErrorCode.DIARY_NOT_FOUND)

3. 조건 검증 로직 단순화

  • Java의 명령형 조건문(if) 위주의 흐름 제어 → Kotlin의 표현식 기반 함수(takeIf)로 변경하여 조건 검증 로직을 간결하게 표현

변경 전 (Java)

@Transactional(readOnly = true)
public void checkCanUpdate(User user, int commentId) {
    ...

    if(!comment.getUser().equals(user)) {
        throw new CustomException(ErrorCode.COMMENT_UPDATE_FORBIDDEN);
    }
}

변경 후 (Kotlin)

private fun checkCanAccess(
  user: User?,
  comment: Comment,
  errorCode: ErrorCode
) {
        ....

  comment.user.takeIf { it == user } ?: throw CustomException(errorCode)
}

4. Lombok 제거

  • Kotlin의 주 생성자와 프로퍼티 문법을 활용하여 Lombok(@Getter, @RequiredArgsConstructor 등)을 완전히 제거
  • Entity의 경우 kotlin-jpa 플러그인을 적용하여 기본 생성자가 자동으로 생성

변경 전 (Java)

// Entity
@Entity
@Getter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Comment {

	...
	
	private String content;
	
	public Comment(Diary diary, User user, String content) {
        this.user = user;
        this.content = content;
        this.diary = diary;
   }
}

// Service
@Service
@RequiredArgsConstructor
public class CommentService {
    private final DiaryRepository diaryRepository;
    private final CommentRepository commentRepository;
    
    ....
}

변경 후 (Kotlin)

// Entity
@Entity
@EntityListeners(AuditingEntityListener::class)
class Comment (
    var content: String,
) {
	...
}

// Service
@Service
class CommentService(
    private val diaryRepository: DiaryRepository,
    private val commentRepository: CommentRepository,
) {

	....
}

성과

  • Java의 Optional 기반 예외 처리 방식 → Kotlin의 nullable 타입과 엘비스 연산자(?:)로 전환, 컴파일 타임 기준의 null 안전성과 코드 간결성 확보

  • Repository 조회 및 예외 처리와 같이 반복되던 로직을 Kotlin 확장 함수로 추상화, 보일러플레이트 코드를 제거

  • Java의 명령형 조건문(if) 위주의 흐름 제어 → Kotlin의 표현식 기반 함수(takeIf)로 변경, 조건 검증 로직을 간결화

  • Kotlin의 주 생성자와 프로퍼티 문법을 활용, Getter, RequiredArgsConstructorLombok 제거

  • 마이그레이션 후 코드 라인 수 240 → 181로 약 24.6% 감소

    구분 Entity Service Controller Total
    Java 57 105 78 240
    Kotlin 42 73 66 181

트러블 슈팅

댓글 조회 : N + 1 문제

문제

  • 댓글 조회 시, 각 댓글의 작성자(User)를 Lazy Loading으로 조회하면서 댓글 수(N)만큼 추가 쿼리 발생

원인 분석

  • Comment → User 다대일 연관관계
  • DTO 변환 과정에서 comment.user 접근 시 프록시 초기화 발생

해결 방법

  • QueryDSL + Fetch Join을 적용하여 댓글과 작성자를 한 번에 조회
override fun findQByDiaryIdOrderByCreatedAtDesc(diaryId: Int): List<Comment> {
    val comment = QComment.comment
    val user = QUser.user

    return queryFactory
        .selectFrom(comment)
        // FetchJoin 적용
        .join(comment.user, user).fetchJoin()
        .where(comment.diary.id.eq(diaryId))
        .orderBy(comment.createdAt.desc())
        .fetch()
}

결과

  • 쿼리 수: 1 + N → 1

  • JMeter 100회 동시 요청 기준

    • 평균 응답 시간 44ms → 13ms (약 70% 개선)

About

감상을 기록하고, 나누는 감성 공유 플랫폼

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 60.2%
  • Kotlin 37.9%
  • HCL 1.0%
  • Java 0.5%
  • Dockerfile 0.1%
  • CSS 0.1%
  • Other 0.2%