미리보기
기본 정보

안녕하세요! Java와 Spring Boot를 다루는 백엔드 개발자 지망생 박철현입니다. 필요한 기술과 지식에 대해서 적극적으로 학습하며 적용하고, 주도적으로 문제를 해결하며 성장하고 회고를 통해 스스로 발전을 희망하는 개발자 지망생입니다.
기술 스택
Java, Spring Boot, JPA, Redis, DBMS/RDBMS, MySQL, REST API
프로젝트
Plist
개인
2024.12. ~ 2025.01.
서비스 설명: 음악 취향이 비슷한 사람들과 소통하며 곡을 공유할 수 있는 실시간 음악 감상 플랫폼.
개발 기간: 24.12.16 ~ 25.01.23(6주).
WebSocket 프로토콜을 활용한 실시간 채팅과 STOMP를 활용한 메시지 라우팅
Redis Pub/Sub을 통해 멀티서버 환경 고려
Github Actions를 활용한 CI/CD 무중단 배포
플레이리스트 비디오 영상 추가 및 순서변경시 발생한 동시성 이슈 - 비관적 락 적용
DTO Projection을 활용한 쿼리 개선
관련기술: Java, Spring Boot, SpringData JPA, MySQL, WebSocket, STOMP, Redis, Github Actions, Nginx, CI/CD, DTO Projection
[기여한 부분]
WebSocket 프로토콜을 활용한 실시간 채팅과 STOMP를 활용한 메시지 라우팅
[도입 배경]
실시간으로 메시지가 오가는 환경에서 매번 HTTP 요청과 응답이 반복적으로 이루어지는 것은 비효율적이고 성능저하가 발생할 것이라고 생각했습니다.
Polling 방식으로 클라이언트가 서버쪽에 주기적으로 요청을 보내서 업데이트를 확인하는 방식 또한 지속적인 서버 응답으로 인해 실시간 채팅과는 맞지 않다고 생각하여 한번만 연결을 맺고 나면 별도의 요청없이 통신이 가능한 WebSocket을 도입하였습니다.
여러 채팅방이 생성될테고 이로 인한 사용자들 또한 웹소켓 세션을 적절히 관리를 해줘야 하는데 STOMP를 도입하여 channel별 구독 정보를 저장하고 클라이언트와 서버가 특정 경로를 기준으로 메시지를 교환할 수 있는 STMOP를 도입하였습니다.
[사용 이유]
WebSocket + STOMP를 활용한 인증 및 세션관리
WebSocket + STOPM는 최초 한번의 연결시 HTTP 헤더를 통해 Token과 같은 인증 정보를 보낼 수 있고 이를 활용하여 유저와 관련된 인증 처리를 하는데에 있어서 용이 할 것이라고 판단하여 적용하였습니다.
WebSocket만을 이용할 경우 세션관리에 대해서 connect/disconnect시 발생하는 이벤트를 처리하여야 했습니다. 여기에 STOMP를 더하면 WebSocket과의 연결을 추척하고 세션 ID를 자동으로 관리해주므로 적용하였습니다.
다중 채널(채팅방) 관리의 필요성
WebSocket은 기본적으로 1:1 연결만 제공하기 때문에 여러 채널을 동시에 관리하는 기능이 부족합니다.
STOMP를 활용하면 특정 토픽을 구독하는 모든 사용자에게 메시지를 자동으로 전달할 수 있어, 다중 채팅방 지원이 용이하였습니다.
[성과]
불필요한 코드 감소 및 유지보수성 향상
WebSocket + STOPM를 사용하여 connect/diconnect 이벤트를 직접관리 하지않고 STOMP에서 처리할 수 있도록 하여 불필요한 코드 작성을 없앰으로써 코드 복잡도를 감소 시켰습니다.
다수의 사용자들이 참여하는 채팅 시스템 구현
기본적인 1:1 채팅 기능에서 다수의 사용자들이 채팅방을 만들고 소통할 수 있도록 하였습니다.
보안 강화(STOMP의 ChannelInterceptor 활용)
STOMP에서 제공하는 ChannelInterceptor를 구현하여, 클라이언트가 서버로 보내는 STOMP 메시지를 가로채어 사용자 토큰을 검증할 수 있도록 설정하였습니다.
Redis Pub/Sub을 통해 멀티서버 환경 고려
[도입 배경]
웹소켓 통신에서 특정 사용자는 특정 서버의 메모리에 의존적이여서 서로 다른 서버에 연결된 사용자들 끼리는 서비스를 원활히 사용할 수 없다는 것을 인지하였습니다.
WebSocket + STOMP와 쉽게 연동이 가능하며 메모리 기반의 빠른 성능이 특징인 Redis를 적극 활용하기로 하였습니다.
[사용 이유]
서버간 사용자 세션 공유
WebSocket은 기본적으로 연결된 서버의 메모리에 종속되므로, 사용자가 서로 다른 서버에 연결될 경우 메시지 동기화가 되지 않는 문제가 있었습니다.
이를 해결하기 위해 Redis의 Pub/Sub을 도입하여 서버간 메시지를 동기화하고 세션을 공유하도록 설계하였습니다.
WebSocket + STOMP와의 높은 호환성
실시간 채팅에서는 RabbitMQ, Kafka와 같은 메시지 브로커를 고려할 수도 있지만, 설정이 간단하고 WebSocket과 쉽게 연동이 가능한 Redis가 적합하다고 판단하였습니다.
러닝 커브가 낮아 프로젝트 적용이 용이하고, 빠른 개발이 가능했습니다.
낮은 지연시간을 위한 최적의 선택
실시간 채팅에서는 낮은 지연시간이 중요하다고 판단했습니다. Redis는 메모리 기반으로 작동하는 NoSQL 데이터베이스로 빠른 성능을 보장하기 때문에 Redis를 택하였습니다
[성과]
멀티서버 환경에서 원할한 채팅 가능
로드밸런서를 활용하는 멀티서버 환경에서도 모든 서버가 동일한 Redis Pub/Sub 채널을 구독하도록 설정하여 사용자가 어느 서버에 연결되었든 동일한 채팅 경험을 제공할 수 있도록 되었습니다.
Github Actions를 활용한 CI/CD 무중단 배포
[도입 배경]
기능 추가나 코드 변경이 발생할 때마다 EC2 서버에 직접 접속하여 수동으로 배포해야 하는 번거로움이 있었습니다.
이 과정에서 수동으로 반복적인 배포과정이 필요했고, 배포 담당자인 저로써는 맡은 기능 개발 역할과 더해서 지속적으로 서버를 관리해야 하는 부담이 컸습니다.
또한 해당 과정에서 실수가 발생할 가능성이 있었으며, 즉각적인 코드 반영이 어려워 운영의 효율성이 떨어지는 문제가 존재했었습니다.
[사용 이유]
무중단 배포 환경 구축
Nginx의 리버스 프록시를 활용하여 무중단 배포를 구현함으로써 새로운 기능 추가나 버그 수정이 발생할 때마다 서비스 중단 없이 최신 버전으로 전환할 수 있도록 CI/CD 자동화를 적용하였습니다.
일관된 배포 환경 유지 및 비용 절감
CI/CD를 통해 배포 프로세스를 표준화하고, 일관된 배포 환경을 유지하고 수동 배포 과정에서 발생하는 운영 비용을 절감할 수 있었습니다.
Github Actions를 선택한 이유
CI/CD 구축을 위해 Jenkins와 Github Actions를 비교해본 결과 현재 프로젝트에서 AWS EC2 프리티어 계정을 사용하고 있기 때문에 메모리 사용이 제한적이었습니다. Jenkins를 사용하기 위한 배포 전용 서버를 구축하기에는 비용이 들었고, 같은 서버에서 Jenkins를 운영하는 것 또한 메모리 부담이 커지므로 별도의 빌드 서버가 필요하지 않은 Github Actions를 선택하게 되었습니다.
[성과]
개발 생산성 증가
자동 배포 시스템 구축 후 배포 과정에서 소요되는 시간이 줄어들었고, 저 또한 개발에 더 집중할 수 있는 환경이 되었습니다. 그리고 팀원들 또한 제가 배포해줄 때까지 기다리지 않고 개발을 진행할 수 있었기 때문에 팀 전체의 업무 효율성이 증가하였습니다.
배포 안정성 강화
무중단 배포를 통해 서버의 다운타임을 없앨 수 있었고, 코드 변경 사항을 안정적으로 반영할 수 있게 되었습니다.
[트러블 슈팅]
플레이리스트 비디오 영상 추가 및 순서변경시 발생한 동시성 이슈 - 비관적 락 적용
[문제 상황]
호스트 및 사용자가 비디오 추가 및 삭제, 순서변경에 대한 이벤트가 동시 다발적으로 발생할 경우 RaceCondition이 발생하며 일부 데이터의 변경이 누락되는 현상이 발생했습니다.
[원인 파악]
해당 프로젝트에서는 실시간으로 플레이리스트 음악 추가 및 순서변경에 대한 기능이 존재하며 이를 같은 채널에 구독중인 사용자들은 현재 플레이리스트 목록을 통해 항상 최신화된 플레이리스트를 불러 올 수 있습니다. 여기서 다수의 요청으로 추가 및 순서변경 작업이 발생하고 최신화된 플레이리스트를 불러오는 과정에서 데이터의 정합성이 깨지는 문제가 발생하였습니다.
이는 공유 자원에 대해서 RaceCondition 발생 시 처음에 자원을 차지한 사용자의 요청이 반영되었다가, 마지막에 자원을 차지한 데이터에 의해서 덮어씌워지는 것을 발견하고 이로 인해 한쪽은 데이터가 누락되는 것을 인지하였습니다.
[해결 방안]
동시성 이슈는 공유자원에 대해서 사용자들의 요청이 몰리게 되는 것이니, 한 쓰레드가 쓰기 작업을 처리할 때 접근하지 못하도록 막아서 데이터의 정합성을 보장하는 것을 이용하였습니다.
[이유]
레코드 락, 낙관적 락, 비관적 락 중에서 비관적 락을 채택하였습니다.
현재 비디오의 추가 및 순서변경 기능은 해당 채널에 참여한 모두가 가능하기 때문에 쓰기 작업 트래픽이 많을 것으로 예상하였고, @Version을 활용하여 데이터가 변경되었는지를 검증 후 업데이트를 진행하는데 충돌 발생 시에 따른 재시도 로직을 추가적으로 작성해야하며 재시도에 의한 추가적인 로직에 의해 오히려 성능이 떨어질 수도 있을 것이라 판단했습니다. 그러한 이유로 낙관적 락은 채택받지 못하였습니다
단순한 데이터 수정에는 적절하지만 순서 변경과 같은 다중 레코드가 연관된 작업이 있을 경우에는 적용하기 어려운 이유로 레코드 락 또한 채택받지 못하였습니다.
여러 사용자가 동시에 수정할 가능성이 높은 작업이기 때문에 충돌이 빈번할 것이고, 조회 시점에서 락을 획득하여 다른 사용자의 변경을 차단하는것이 안전할 것이라고 판단하여 비관적 락을 채택하였습니다.
조회 시에 레코드를 점유하기 때문에 데드락 발생시의 문제점에 대비해서 락을 획득하는 순서를 고려하여 데이터 변경 메서드 호출 발생 직전에 락을 잡는 것으로 데드락 발생 가능성을 줄이고자 하였습니다.
DTO Projection을 활용한 쿼리 개선
[문제 상황]
User API를 담당한 팀원이 로그인 시 쿼리가 예상과 다르게 발생한다고 보고하여 함께 분석을 진행하였습니다. 확인 결과, 로그인 시 호출되는 findByUserEmail() 메서드를 통해 User 엔티티를 조회할 때, 불필요하게 Participant 테이블까지 함께 조회되는 문제가 발생하고 있음을 확인하였습니다.
[원인 파악]
해당 프로젝트에서는 User와 Channel의 테이블을 연결하는 중간 테이블로 Participant 테이블이 존재했습니다.
User와 Participant는 @OneToOne 양방향 매핑이 되어있으며 Participant가 User의 외래키를 관리하고 있었습니다.
문제의 핵심은 JPA의 기본 동작 방식 때문이었습니다.
findByUserEmail() 호출 시 User 엔티티를 로드하면서 JPA가 자동으로 User의 필드 값을 채우는 과정에서 User가 Participant를 필드로 가지고 있기 때문에 추가적인 조회 쿼리가 발생한 것이었습니다.
[해결 방안]
양방향 매핑 해제
가장 간단한 해결책은 User와 Participant간의 양방향 매핑을 단방향으로 변경하는 것이었습니다.
하지만 Channel API를 개발한 팀원이 이미 해당 매핑을 기반으로 기능을 구현한 상태였으며, 이를 수정할 경우 NPE 발생 가능성이 있었습니다.
또한 프로젝트 마감 기한이 얼마 남지 않은 관계로 채널 API의 리팩토링 소요 시간을 예측하기 어려운 상황이었기 때문에 해당 방법은 적용하지 않았습니다.
네이티브 쿼리 작성
네이티브 쿼리를 직접 작성하면 Participant에 대한 데이터를 제외한 User 정보만 조회할 수 있었습니다.
하지만 특정 DBMS에 종속적이 되는 문제가 있으며, 유지보수성이 낮아질 우려가 있어 이 방법도 채택하지 않았습니다.
SpringData JPA DTO Projection
API 스펙에 맞는 데이터만 반환할 수 있도록 DTO Projection을 활용하여 User 데이터를 조회하였습니다.
JPQL을 사용하므로 특정 DBMS에 종속되지 않으며, 네이티브 쿼리와 동일하게 필요한 데이터만 가져올 수 있다는 장점이 있었습니다.
또한, 직접적인 SQL을 작성하지 않아도 되므로 유지보수성이 높아지는 효과를 얻을 수 있었습니다.
단점 : DTO Projection은 API 스펙에 맞춰 정의되기 때문에, 클라이언트 요구사항이 변경되면 DTO도 함께 수정해야 하는 단점이 있습니다.
포트폴리오
대외활동
우테코 프리코스
기타
2024
우테코 프리코스(배달의 민족인 우아한 형제에서 진행하는 IT 개발 인원 모집 프로젝트
프로젝트 개요: 순수 Java 기반으로 진행되며, 객체지향 설계, 다형성, 추상화 및 클린 코드 작성 능력을 평가하는 테스트.
참여기간 : 2024.10.~2024.11
배운점
코드 품질 향상 : SOLID 원칙을 기반으로 유연한 구조 설계를 고민하며 StreamAPI, Optional, Enum 클래스의 활용 및 전략 패턴, 싱글톤 패턴의 디자인 패턴을 활용하여 가독성과 유지 보수성을 높이는 코드 작성법에 대해서 끊임없이 고민하였습니다.
협업 및 코드 리뷰 경험 : 매주 스터디원들과 코드 리뷰를 진행하며 다양한 접근 방식을 비교하고, 서로의 코드를 개선하는 과정을 통해 개발 역량을 증진하고 더 넓게 보는 시야를 길렀습니다.
지속적인 학습 태도 : 프리코스 이후에도 여러가지 사이드 프로젝트와 “객체지향의 사실과 오해” 북 스터디를 통해 객체지향의 본질과 소프트웨어 개발의 본질에 다가가기 위해 학습했습니다.
자기소개
안녕하세요. Java와 Spring Boot를 활용하여 문제를 주도적으로 해결하고, 지속적인 학습과 성장을 통해 더 나은 가치를 만들어가는 백엔드 개발자 지망생 박철현입니다.
저는 기존에 안정적인 공무원 생활을 하고 있었으나, 문제를 직접 해결하며 성취감을 느낄 수 있는 개발자의 길에 도전하게 되었습니다. 업무 중 매뉴얼 검색 과정의 비효율성을 발견하고 Java의 I/O Stream을 활용해 직접 매뉴얼을 효율적으로 관리할 수 있도록 개발했던 경험이 개발자로 전향하게 된 결정적인 계기였습니다.
이후 Java 언어를 기초부터 독학하며 개발자로서의 역량을 쌓아갔습니다. 우아한형제들에서 주관한 '우테코 프리코스'에 참여하면서 객체지향 설계, SOLID 원칙을 활용한 유연한 코드 설계, Stream API, Optional, Enum 클래스 활용법 등을 깊이 있게 배우고 실습했습니다. 또한 스터디원들과 코드 리뷰를 진행하며 다양한 관점에서 코드 품질을 개선하는 협업 경험도 얻었습니다.
이를 기반으로 팀 프로젝트에서 실무와 가까운 환경을 경험하며 협업 능력을 키웠습니다. 특히 '음악과 사람을 잇는 실시간 음악 감상 플랫폼' 프로젝트에서 WebSocket과 STOMP를 활용하여 실시간 채팅 기능을 구현했습니다. STOMP의 ChannelInterceptor를 이용한 사용자 인증 및 세션 관리를 통해 보안성을 강화했고, Redis Pub/Sub을 도입하여 멀티 서버 환경에서도 원활한 서비스가 제공되도록 설계하였습니다. 또한, CI/CD 구축 시 Jenkins 대신 Github Actions를 활용하여 배포 자동화와 무중단 배포 환경을 구축함으로써 배포 과정에서의 효율성과 안정성을 높였습니다.
이 과정에서 플레이리스트 기능 구현 시 발생한 동시성 이슈를 JPA의 비관적 락으로 해결하고, DTO Projection을 활용하여 성능 개선 및 불필요한 데이터 조회 문제를 해결하는 등 적극적으로 문제를 찾아 해결하며 성장했습니다.
저는 개발 과정에서 얻은 모든 경험을 블로그와 회고록으로 문서화하여 스스로의 성장을 점검하고 있습니다. 앞으로도 끊임없이 발전하며 주도적으로 문제를 해결하는 개발자가 되고자 합니다. 감사합니다.