코코코딩공부
이미지 리사이징
들어가며 ‘요즘카페’ 서비스를 배포 하고 설문을 통해 사용자들로부터 피드백을 받았다. 피드백의 내용으로는 이미지 로딩 속도 및 용량 관련된 것이었다. 우리는 보여지는 사진의 이미지의 화질이 좋아야 한다고 생각했다. 그래서 원본 데이터를 그대로 전달하고 있었는데 이것이 문제가 되었다. 그래서 이미지 로딩 시간이 길며 사용성을 해치고 있었다. 이를 위해 이미지를 리사이징하여 S3에 저장하고, 필요시 서빙하도록 했다. 리사이즈 툴 marvin thumbnailator Imgscalr 2DGraphics getScaledInstance 과 같은 리사이즈를 위한 도구들이 존재한다. 처음 3개는 외부 라이브러리이며, 나머지 두개는 java 내부 라이브러리이다. 외부 라이브러리에 의존하지 않고 java 내부 라이브러..
공간데이터와 공간인덱스로 지도 개선하기
들어가며 요즘카페 서비스에 지도기능을 추가하기로 하였다. 사용자 설문조사 결과 지도와 관련된 피드백이 많았기 때문에 카페를 지도에 나타내기로 하였다. 초기에는 다음과 같이 좌표를 기준으로 원을 그리고 이를 탐색하기로 하였다. 원 안에 그려진 카페들을 핀을 통해서 보여주는 방식이다. 우리 서비스는 DB로 MySql을 사용하고 있는데 지도를 사용하기 위해 공간 정보를 담을 수 있는 공간데이터, 공간데이터를 효율적으로 사용할 수 있는 공간 함수, 검색 성능을 올릴 수 있는 공간 인덱스가 존재한다. 이를 활용하여 요즘카페 서비스의 지도 기능을 성능 좋게 만들어 보겠다. MySQL 공간 데이터 다루기 MySql에는 여러가지 공간 데이터가 존재한다. GEOMETRY POINT 좌표 공간의 한 지점 LINESTRIN..
Batch Insert에 대한 고민(feat. 2배 개선)
들어가며 현재 요즘카페 서비스는 조회하지 않은 카페 데이터를 저장하고 있다. 이를 통해서 사용자에게 중복되지 않은 랜덤한 이미지를 전달해준다. 사용자가 조회를 진행하면서, 조회하지 않은 카페 데이터가 특정 수 이하가 되면 새롭게 카페를 삽입해준다. 사용자가 많아짐에 따라 이런 과정은 빈번하게 발생하여 서비스의 성능저하를 일으켰다. 기존 코드 기존에는 데이터 삽입 시 JPA의 변경 감지를 이용하여 요청을 단 건으로 처리하였다. 그래서 데이터가 늘어남에 따라 단일 삽입쿼리도 늘어나고 실행되는 시간도 길어졌다. 그래서 이를 개선하고자 데이터 삽입 할 때 Batch Insert를 하기로 하였다. 현재와 같은 구조인데 100여개의 카페 삽입 로직 때문에 Query Count가 높게 발생하는 것을 볼 수 있다. 해..
필터를 사용해 API 성능 로그 만들기
들어가며 요즘카페 서비스를 개발하면서 우리가 만든 API가 잘 작동하는지 검증할 필요가 있었다. API 별로 테스트를 통해 잘 동작하는지 검증할 수는 있지만 실제 돌아가는 환경에서 확인하고자 하였다. 이를 통해 API 별로 발생할 수 있는 쿼리, 수행 시간, 성능 등을 측정하고자 하는 필요성이 느껴져 진행하게 되었다. 로그로 남기고자 하는 데이터는 요청 URI, Method, 응답까지 걸리는 시간(ms), 발생 쿼리 횟수를 남기고자 하였다. 요청 URI, Method 로그로 요청 URI를 남기면서 어떤 요청에 대해 예외가 발생했고, 요청 빈도 수는 어떻게 되는 지를 알아차릴 수 있다. @Component public class LatencyLoggingFilter extends OncePerRequest..
테스트를 더 빠르게 진행시켜보자
ApplicationContext 개선 우리의 서비스는 TestContainer 를 사용중인데 이로 인해 테스트 실행시 컨테이너가 실행되며 처리되는 시간이 길어졌다. 이런 테스트 속도를 개선하고자 하였다. Application Context Spring TestContext Framework는 ApplicationContext 인스턴스와 WebApplicationContext 인스턴스를 로딩하고 캐싱하는 기능을 한다. 이를 통해 한 번 로드된 컨텍스트를 캐시에 저장해두고 동일 환경 시 해당 컨텍스트를 재사용한다. 만약 이전 테스트와 다른 환경의 컨텍스트가 필요하다면 컨텍스트가 로딩된다. 현재 우리 서비스는 총 11개의 Context 생성된다. 테스트를 진행하면서 다른 환경의 컨텍스트가 빈번하게 발생할 경..
TestContainer 사용기 & 테스트 격리
들어가며 요즘카페 서비스는 현재 TEST환경에서 DB MySql을 사용하고 있다. 이 과정에서 여러 문제점이 있었다. 테스트가 작동하는 모든 환경에 MySql을 설치하여야 한다. Teamcity CICD 빌드 과정에서 Docker In Docker로 MySql을 띄워야한다. 이런 불편함 때문에 관리 포인트가 적은 TestContainer를 사용하게 되었다. TestContainer 테스트 컨테이너 라는 것은 자바 코드를 통해서 도커 컨테이너를 제어하여 테스트를 도와주는 라이브러리이다. https://testcontainers.com/ 이를 통해 테스트에 필요한 다른 의존성들을 제어할 수 있다. 생각보다 공식문서 자료가 잘 되어있다. 테스트컨테이너란 코드로 도커 컨테이너를 제어하여 통합테스트를 도와주는 라..
DB Replication
들어가며 현재 요즘카페 팀의 DB는 서버와 같은 EC2에 존재하기 때문에 SPOF가 있어서 이를 제거하고자 하였다. 그러기 위해 DB를 다른 서버에 두기로 하였다. 이 과정에서 요즘카페 는 조회가 빈번하게 발생하고, 조회 도중에도 시청하지 않은 카페를 삽입해주는 로직이 실행되기 때문에 조회와 삽입을 DB Replication을 통해 성능을 향상시키고자 하였다. Replication 한 서버에서 다른 서버로 데이터가 동기화 되는 것을 의미한다. 원본 데이터를 가진 소스서버에서 변경이 발생하면 복제 데이터를 갖는 레플리카 서버에는 이러한 변경 내역을 토대로 레플리카 서버의 데이터로 반영한다. 이 Replication을 하는 다양한 이유가 존재하는데 스케일 아웃: 서버 분리로 트래픽 분산 데이터 백업: 레플리..
DataSource 라우팅이 안되는 이유. OSIV
들어가며 DB 레플리케이션을 통한 성능 개선을 진행하였다. MySQL과 스프링부트 환경에서 레플리케이션을 적용하였는데 이는 추후 블로깅 예정이다. DB 레플리케이션을 위해서는 스프링 부트에서 여러 DataSource를 설정하고, 트랜잭션의 옵션에 따라 특정 DataSource로 분기처리를 해야한다. 이를 적용하려고 할 때 문제가 발생했다. 서비스 메소드의 readOnly = false 메소드가 읽기전용 db로만 쿼리가 날아간 것이다. 우리의 예상대로면 readOnly = false 일 경우 쓰기 db, readOnly = true 일 경우 읽기 db 로 날아가야한다. 하지만 문제가 발생했다. 문제 발견 우리의 요즘카페 서비스에서는 로그인한 사용자가 조회 하며 조회 기록 데이터를 수정하고, 좋아요 처리를 ..
Flyway 적용해보기
들어가며 프로젝트를 진행하며 중간중간 db 구조 및 인덱스 등등이 달라짐에 따라 db 버전관리를 해야할 필요를 느꼈다. db의 버전 관리를 자체 문서 등을 통해 실행할 수 있지만 실수의 여지가 많다고 판단되었고 누군가가 스키마 변경을 실행해놓고 기록해두지 않느다면 놓칠 가능성이 크기때문에 우리는 버전관리 flyway를 적용해보기로 하였다 flyway 란 ? DDL, DML 등 스키마 변경을 정해둔 규칙에 따라 자동으로 DB에 적용해주는 DB 형상관리 툴이다. 동작 방식 flyway를 적용하면 flyway_history 테이블에 그 시점에 테이블 스키마에 대한 메타 데이터가 기록된다. 이후 flyway는 사용자가 정의한 sql 파일을 scan해서 migration의 버전과 작업을 인지한다. 만약 새로 정의..
N+1문제 개선기
들어가며 요즘카페 프로젝트를 진행하면서 개발자가 의도하지 않은 쿼리가 JPA로 인해 발생하는 문제점이 발생했다. 이를 개선하고자 한다. 엄청난 쿼리 카운트 ..................... 이걸 수정해보자 N+1 우리는 1개의 쿼리를 날렸을때 N개의 쿼리가 추가로 발생하는 것을 N+1 문제 라고 한다. ORM 기술인 JPA가 등장하면서 쿼리가 자동화되면서 발생하는 문제점이라고 할 수 있다. 객체에 대해 조회했을 경우에 연관관계매핑으로 관계 맺어진 다른 객체가 조회되는 것이다. 발생 이유 N+1 문제가 발생하는 이유는 JPA가 JPQL에서 SQL 생성할 때 Fetch 전략을 참고하지 않고 JPQL 자체만을 사용하기 때문에 발생한다. fetch = FetchType.LAZY 일 경우 findAll() ..