우아한테크콘서트 2020 후기

2020년 12월 20일 (11달 전)
이번에 우연히 광고를 통해 알게 되어서 태어나서 처음으로 테크 콘서트를 참여할 수 있게 되었다. 코로나 시국이라 비대면으로 진행하게 돼어 어떻게 보면 나에게는 기회가 되었는데, 라인업을 보면 쟁쟁하신 분들이 많아서 보는 내내 나도 모르게 설레었다. 그래서 12월 18일까지 진행했던 우아콘에서 들었던 내용들을 정리해보면서 느낀점 등을 적어보려고 한다.우아콘 오프닝 영상

송재하님 세션(지난 4년간의 Key Milestones)

가장 첫번째 세션이었고, 4분 정도 분량이었지만 이번 우아콘을 통해 전하고 싶은 핵심들을 전달하는 세션이 아니었을까 싶다. 우아콘의 취지 설명과 함께 우아한 형제들의 시스템이 어떻게 변화해왔는 지를 간략하게 설명하는 시간이었다. ASP기반의 스토어드 프로시저로 덮혀있던 시스템을 모두 JPA기반으로 자바 스프링 전환한 이야기, 스토어드 프로시저를 모두 걷어내어 MSA 형태로 모두 옮기고, 이어서는 모든 시스템을 AWS로 옮기고 최근에는 ERP마저 클라우드로 전향하였고, 클라우드 상에서 전자금융 시스템을 실제로 구축한 사례도 만들면서 우아한 형제들은 내외부 모두 클라우드 네이티브 환경으로 운영된다고 한다. 첫번째였던 만큼 가장 짧고 굵으면서 발표자분의 자신감이 엿보이는 세션이었던 것 같다.

김영한님 세션(배민 마이크로서비스 여행기)

이 세션은 배달의 민족이 마이크로서비스를 도입하면서 겪었던 비하인드 스토리? 같은 것들의 내용과 함께 마이크로서비스를 도입하면서 겪었던 이야기들을 설명해주셨다. 가장 재미있게 들은 내용은 쿠폰 시스템에 대한 내용이었는데, 쿠폰을 뿌리게 되면 잠시 동안 사용자가 엄청 몰리게 되어 부하를 견디지 못해 죽는 서버 문제를 해결하기 위해 긴급하게 장비 수급은 어려워서 AWS는 빠르게 증설이 가능하기 때문에 AWS에 모든 서비스를 올리게 되고, 몇 일만에 성공하게 되고 개발자들이 모두 엄청 기뻐했다는 이야기였는데, 나도 모르게 공감하게 되면서 재미있게 본 세션이었다.

권용근님 세션(배민 프론트 서버의 사실과 오해)

이 세션은 권용근님이 배민에 오기 전까지 혹은 프론트서버를 맡기 전까지 가지고 있던 프론트 서버의 오해에 대해서 다루는 세션이었다. 프론트 서버라고 하면 리액트를 돌릴 때 사용하는 그런 프론트서버를 말하는 줄 알았는데, 여기서 프론트 서버는 사용자와 가장 가깝게 맞닿아있는 서버를 말한다고 한다.

첫번째 오해-프론트 서버는 단순하다

처음에는 단순히 Spring+MVC+Redis 정도의 구조일거라 생각했지만 프론트 서버에는 두가지 특징이 있었으니…

  • 수 많은 외부 서비스의 실시간 상태가 필요
    • 동시 요청+많은 트래픽+좋은 사용자 경험을 위해 비동기+논블로킹 I/O 기술을 지원하는 SpringWebFlux 도입
  • MSA의 최전방
    • 각종 요구사항에 맞는 디비들을 적재적소에 배치 + 서비스 간 결합을 제거하여 정확성을 높임(MSA+CQRS)

결론

프론트 서버는 결코 단순하지 않다.

두번째 오해-프론트 서버는 객체, 도메인 중심적인 개발할 수 없다.

레이어 관점에서 Repository단의 데이터는 내 데이터가 아니기 때문에 Service를 통해 데이터를 조합해서 사용자에게 전달될 수 밖에 없기 때문에 객체지향적이나 도메인적으로 풀어나갈 수 없지 않나?라는 생각을 가지셨다고 한다. 여기 두번째 오해에 대해서 들으면서 점점 정신이 멍해졌는데… 정리하자면 Service-Repository 간에서는 도메인, 객체지향 형태의 개발이 어렵기 때문에 안정된 의존 관계 원칙(SDP), 안정된 추상화 원칙(SAP), 의존 역전 원칙(DIP) 등을 적용해서 중간에 Domain을 추상화해 껴넣음으로써 도메인, 객체지향 형태로 개발할 수 있도록 했다는 내용이었다.

이 세션을 들으면서 클린 아키텍처라는 책은 꼭 한번 읽어봐야 겠다는 생각이 들었다. 클린 아키텍처를 읽고 해당 세션의 내용을 다시 한번 되새겨봐야 겠다.

이동욱님 세션(수십억건에서 Querydsl 사용하기)

다른 분들은 직접적인 코드가 아닌 어떤 노하우나 경험, 아키텍처 등에 대해서 말씀해주셨다면, 이동욱님은 직접적인 코드를 통한 경험을 전수해주셨다. 이 파트는 워낙 관심 가졌던 주제라 열심히 정리해보았다.

Querydsl을 설정 팁

  1. 상속과 구현을 통한 방법
  2. Support를 상속받고, Supper 생성자에 Entity를 등록하는 방법

위의 두가지 방법은 매번 상속구현, Support상속-엔티티 등록 해야 하는 번거로운 문제가 있었는데, JPAQueryFactory만 빈을 주입해도 Querydsl 사용이 가능하다.

동적 쿼리 팁

일반적으로 Boolean Builder를 통해 동적쿼리를 사용하는데, Boolean Builder를 사용할 경우 조건문이 너무 늘어남에 따라 가독성이 안 좋기 때문에 어떤 쿼리인지 예상이 어려운 문제가 있었다.

이를 해결하기 위해 Boolean Expression을 사용하는데, 메소드 형태로 만들어서 해당 값이 null 반환이 될 경우 자동으로 조건이 제거되도록 처리를 해주면 된다.

Querydsl에서 exists는 직접 구현해서 사용하자

SQL에서 exists와 count의 성능 차이

exists는 첫번째 값이 발생되면 검색을 멈추는데, count은 첫번째 값이 발생하더라도 마지막까지 모든 조건을 체크하기 때문에 스캔 대상이 앞에 있을수록 더 심한 성능차이 발생한다.

exists를 개선?

Querydsl의 exists는 SQL.count를 사용한다. 그리고 Querydsl는 from절의 서브쿼리가 지원되지 않기 떄문에 개선할 수도 없다. 왜냐하면 exists는 where절에서만 사용 가능하다.

그럼 exists를 만들어서 쓰자

구현 조건

조건에 해당하는 row 1개만 찾으면 쿼리 종료

구현 방법

limit 1로 조회를 제한, 이 때 조회 결과가 없으면 null이 반환되기 때문에 null인지를 체크해주자. 자세한 내용

결과는?

실제 SQL.exists와 성능 차이가 거의 나지 않는다.

Cross Join을 회피하자

왜 안좋은가?

일부 DB에서는 어느정도 최적화가 지원되지만, 나올 수 있는 모든 경우에 대해서 조인되기 때문에 DB에 기대하지 말고 왠만하면 회피하는 것이 좋다.

회피 방법

묵시적 Join을 사용할 경우 Cross Join이 발생하기 떄문에 명시적 Join을 사용하여 Inner Join이 발생하도록 하면 된다.

Entity보다 Dto를 이용할 경우의 최적화 방법

자세한 내용

엔티티 조회 시 문제점

단순 조회 기능에서는 성능 이슈가 많은데,

  1. 하이버네이트 캐시 문제(1차 캐시, 2차 캐시 등)
  2. 불필요한 컬럼 조회
  3. OneToOne 연관관계에서의 N+1 발생

조회 컬럼 최소화하기

이미 알고 있는 값에 대해서 as 표현식으로 대체하자(select에서 제외됨)

Select절에 컬럼 엔티티 자제하기

Select절에 엔티티를 사용할 경우 문제점

  1. 사용하지도 않는 컬럼임에도 굳이 조회가 발생
  2. OneToOne 연관관계가 맺어져 있을 경우 Lazy Loading이 안되기 때문에 무조건 N+1 무조건 발생
  3. 엔티티 컬럼 전체가 Distinct 대상이 되어 distinct를 위한 임시 테이블 공간과 시간이 더 필요하기 때문에 기존 대비 성능 많이 떨어짐

결론

진짜 필요한 엔티티가 아니라면 Querydsl과 Dto를 통해 필요한 항목들만 조회하고 업데이트 한다.

Group by 최적화 방법

일반적으로 Mysql은 Group by 진행 시 filesort가 필수적으로 발생함. 이 경우는 해당 쿼리가 인덱스를 타지 않았을 경우인데, 모든 쿼리가 인덱스를 탄다는 보장은 없기 때문에 filesort가 발생하지 않도록 하자. mysql에서는 order by null을 사용할 경우 filesort가 제거된다.

문제

Querydsl에서는 order by null 문법을 지원하지 않음.

해결 방법

order by null 기능을 구현해서 사용. 이동욱님은 OrderByNull이라는 클래스를 구현해서 해당 클래스를 OrderBy 문법에 넣어주는 방법으로 order by null이 발생하도록 하셨다고 한다. 자세히보기

정렬이 필요하더라도 조회 결과가 100건 이하라면 WAS에서 정렬하는 것을 추천한다. 왜냐하면 DB보다 WAS쪽 자원이 여유롭기 때문. 하지만 페이징을 이용한다면 order by null 사용이 불가함.

Querydsl에서 커버링 인덱스를 사용하는 방법

커버링 인덱스란?

쿼리를 충족시키는데 필요한 모든 컬럼을 갖고 있는 인덱스.

쓰는 이유

NoOffset방식과 더불어 페이징 조회 성능을 향상시키는 가장 보편적인 방법

Querydsl에서는 사용 불가

왜냐하면 from절 서브쿼리를 지원하지 않기 때문에 사용 불가

우회한 방법으로 구현

클로저 키(PK)를 커버링 인덱스로 빠르게 조회하고, 조회한 Key로 Select 컬럼들을 후속 조회한다. 결국, 하나의 쿼리로 할 수 있는 것을 두번으로 나눠서 조회하라는 내용이다.자세히보기

효과

기존 커버링 인덱스와 거의 비슷한 성능을 보여준다.

업데이트 최적화 방법

일반적으로 객체지향을 핑계로 더티 체킹 방식으로 모든 값이 업데이트 되는 것이 아니더라도, 모든 컬럼을 조회 후 업데이트를 치게 된다.

더티체킹의 문제를 일괄 업데이트를 통해 해결하는 방법

Querydsl.update를 통해 한번에 업데이트를 치는게 좋음.자세히 보기

효과

1만건 기준으로 2000배 정도 차이가 남.

일괄 업데이트의 단점은?

하이버네이트 캐시는 일괄 업데이트 시 캐시 갱신이 안됨. 이럴 경우 업데이트 대상들에 대한 Cache Eviction이 필요

용도에 구분해서 사용하자

  • 더티 체킹 - 실시간 비즈니스 처리, 실시간 단건 처리 시 효율적
  • Querydsl.update - 대량의 데이터를 일괄로 Update 처리 시 효율적

하이버네이트 캐시 갱신이 필요없는 서비스에서만 Querydsl.update를 고려하는게 좋음.

Bulk Insert는 자제하자

이유

JDBC에는 rewriteBatchedStatements를 사용하는데 Insert합치기 옵션을 넣어도, JPA는 auto_increment일 때 insert 합치기가 적용되지 않는다.

JdbcTemplate를 사용한 개선 방법

JPA를 쓰기보다, JdbcTemplate으로 처리하면 됨. 하지만 컴파일 체크, 코드-테이블 간 불일치 체크 등 Type Safe한 개발이 어려운 단점이 있음.

JdbcTemplate의 단점을 보완한 방법

지금까진 Querydsl-JPA를 사용한 방법인데, Querydsl-JPA는 Querydsl의 하위 프로젝트로, 다른 Querydsl-SQL 프로젝트 통한 엔티티QL을 사용한 방법인데, JPA엔티티 기반으로 Querydls-SQL QClass 생성하고, 엔티티QL로 만들어진 Querydsl-SQL의 QClass를 사용하면 BulkInsert가 가능해진다.

결과

  1. 단일 엔티티 1만건 insert 시 135배의 차이가 발생.
  2. OneToMany 같은 경우에도 약 6배 정도 발생

하지만…엔티티QL의 단점은 꽤 많았으니…

  1. Gradle 5 이상 필요(플러그인이 gradle 5 이상에서만 작동)
  2. 어노테이션에 (name=“”) 필수
    1. @Column의 name값으로 QClass 필드가 선언되기 때문
    2. @Table의 name값으로 테이블을 찾을 수 있다.
  3. primitive type 사용 불가
    1. 기본형 int, double, boolean 등 사용 불가.
    2. 모든 컬럼은 Wrapper Class로 선언해야 한다.
  4. Querydsl-sql이 개선되지 못해 불편한 설정 많음
  5. @Embedde 미지원
    1. @Embedded 어노테이션을 통합 컬럼 인식 불가
    2. @JoinColumn 미지원
  6. Querydsl-SQL의 미지원으로 insert쿼리를 @Column의 name으로 만들 수 없음(컬럼명과 필드명이 일치해야 하는 BeanMapper만 지원) 즉 @Column용 Mapper가 별도로 필요
  7. Querydsl-SQL 설치의 번거로움
    1. 로컬 PC에 DB 설치 후 실행
    2. Gradle/Maven에 로컬 DB 정보 등록 후 flyway로 테이블 생성
    3. Querydsl-SQL 플러그인으로 테이블 Scan해서 QClass 생성

이렇다 보니 이동욱님도 권장한다기 보다 최악이 아닌 차악을 고른다는 마음으로 사용하신 기술이라고 하셨음. 그만큼 JdbcTemplate를 사용한 방법이 최악이라는 것…

마지막으로

  1. 상황에 따라 ORM / 전통적 쿼리 방식을 골라 사용
  2. JPA / Querydsl로 발생하는 쿼리 한번더 확인하기
  3. 두권의 책(JPA, 리얼 Mysql) 추천

느낀점

이렇게 개발자들을 위한 콘서트를 참가해본 건 이번 우아콘이 처음이었는데, JVM 기반 언어로 개발하면서 우아한 형제들이 업계에 끼치는 영향은 무시할 수 없는 수준인 것 같다. 이번 우아콘을 들으면서 우아한 형제들에서 그 동안 엄청 많은 도전을 해왔고, 그 도전들이 긍정적이든 부정적이든 많은 개발자들에게 영감을 주고, 한편으로는 강한 동기부여를 해주었던 것 같다. 가장 기억에 남는 문구는 이동욱님이 말씀하신 마지막 말씀인데, **“상황에 따라 기술을 골라 사용할 것”**인데 당연한 말 같으면서도 나도 모르게 간과하고 있던 사실인 것 같다. 어떤 기술을 하나 파게 되면, 나도 모르게 그 기술을 맹신하게 되는 경우가 있는데 어떤 기술이든 장단점이 있고, 적재적소에 맞는 기술을 적용하는 것은 개발자에게 매우 중요한 역량이 아닐까 싶다. 이번 우아콘을 들으면서 나 자신에게 또 다시 동기부여를 하게 만드는 귀중한 시간이었던 것 같다.

태그#Review