Browse Tag

@Controller

레저큐 인턴기 6편 – 백엔드 개발 도전! (2)

웹API 컨트롤러의 레이어드 아키텍쳐

전편에서 이어집니다…

가자고 웹API의 컨트롤러는

  • Controller 레이어: 가장 상위에서 유저의 리퀘스트(request)를 받고 리스폰스(response)를 돌려줌.
  • Service 레이어: Controller 밑에서 실질적인 비지니스 로직의 구현을 담고 있음.
  • Repository 레이어: DB와의 인터페이스를 담당.

이상의 세개 레이어(layer)로 구성되어 있다. 이렇게 하나의 시스템을 여러개의 레이어(층)로 분리하고 차곡 차곡 쌓아 올라가는 식으로 개발하는 방식을 ‘레이어드 디자인 패턴(Layered Design Pattern)’이라고 부르는데, 소프트웨어 엔지니어링에서 가장 보편적인 디자인 패턴 중 하나이다.

여러가지 이유가 있겠지만 이렇게 레이어를 나누는 가장 중요한 이유는

  1. 규모가 큰 시스템을 여러개로 모듈화하여 한 모듈의 문제가 시스템 전체로 번져나가는 것을 막음과 동시에 각 모듈을 쉽게 교체하기 위함이자, (종속성의 최소화, 재사용성)
  2. 오로지 하나의 이슈를 해결하도록 분리된 각 층을 추상화함으로써 컴포넌트 간에 인터페이스를 쉽게 하고 개발을 용이하게 하기 위함이다. (표준의 지원)

Screenshot 2016-09-06 14.59.49.png
가자고 웹API의 컨트롤러는 Controller, Service, Repository 등의 레이어로 구성된 레이어드 아키텍쳐(Layered Architecture)를 따른다.

레이어드 아키텍쳐 관점에서 개발 미션 바라보기

이쯤에서 나의 개발 미션이 무엇이었는지 다시 떠올려보자.

입력한 조건에 따라 DB에서 서비스 운영에 필요한 통계자료를 추출하는 웹 API를 새로 만들기

개발 미션을 가자고 웹API 컨트롤러의 레이어드 스트럭쳐 관점에서 바라보면 구현 단계를 가장 밑에서부터 네단계로 쌓아 올라가는 것으로 압축할 수 있다.

  1. Model 클래스 구현: 쿼리로 조회한 데이터를 담을 모델 클래스 만들기.
  2. Repository 구현: 데이터 추출 SQL 쿼리를 자바로 구현하기.
  3. Service 구현: 비지니스 로직을 코드로 구현하기. 즉, Repository에서 쿼리로 가져온 데이터들을 어떻게 가공하고 Controller에 다시 돌려줄 것인지 고민하기.
  4. Controller 구현: API에 URL 자원을 RESTFul하게 할당하고 유저의 리퀘스트 처리. 리스폰스 리턴.

사실 Repository 레이어에서 DB로부터 데이터를 가져오는 것을 빼고는 복잡할 것이 없는 작업이다.

  • Model 클래스에서는 내가 필요한 정보(DB의 필드에 해당한다)가 무엇인지 고민하고 이 값들을 담을 수 있는 변수들을 멤버로 가지고 있는 클래스들을 새로 만들어주면 된다. (클래스 만들기는 자바의 기본!)
  • Service에서는 Repository에서 돌려준 데이터들의 묶음(Collector: List, Map 등…)을 엑셀 템플릿과 엑셀 라이브러리를 이용해서 엑셀 파일로 출력해주면 된다. (엑셀 라이브러리만 가져다 쓰면 된다!)
  • Controller는 사실상 스프링 프레임워크에서 제공되는 라이브러리들을 이용하면 자바 어노테이션과 몇 줄 안되는 코드로 구현이 가능하다. (스프링이 알아서 다 해준다…)

자 그렇다면 이번 개발 미션의 핵심이 무엇인지 드러난다. Repository에 SQL SELECT 쿼리를 자바로 구현하는 것이 이번 개발 미션에서 가장 중요한 부분이다.

JOOQ로 SQL SELECT 쿼리를 자바로 구현하기

가자고 어플리케이션은 DB 인터페이스에 JPA(Java Persistence API: 자바 영속성 API)와 JOOQ(Java Object Oriented Querying)를 이용한다.

JPA는 자바 진영의 표준 ORM(Object-relational mapping) 기술로, 관계형 DB를 객체지향패러다임 언어인 자바에서 쓰기 위한 맵핑을 담당하는 기술이다. 표준 영속성(여기서 영속성이란 프로그램이 종료되어도 사라지지 않는 속성, 즉 DB 데이터의 속성을 의미한다) API인 만큼 자바를 이용해서 백엔드 개발을 하는 개발자들이 익히 잘 알고 있는 기술이다.

반면에 JOOQ는 다소 생소한 이름일 수 있다. 스위스 취리히 소재의 스타트업 Data Geekery이 운영하고, 개발하고 있는 JOOQ는, SQL 쿼리를 자바로 구현해야 하는 상황에 이용하는 라이브러리이다. 즉, 미리 작성해 놓은 SQL 쿼리를 그대로 자바로 옮기고 싶다거나, 아니면 JPA로는 구현이 힘든 매우 복잡한 SQL 쿼리를 짜야하는 경우에 멋진 대안이 된다. (참고로 이런 목적으로 사용하는 또 다른 라이브러리 중에 유명한 것이 iBatis 란 기술이다. 가자고 시스템에도 iBatis를 사용한 적이 있었지만 지금은 대부분 JOOQ로 대체하고 있다.)

좀 더 와닿는 코드 예제로 살펴보자. 백엔드 어플리케이션에서 아래와 같은 SQL 쿼리를 사용할 일이 있다고 가정하자. (코드 출처는 wikipedia.org – Java Object Oriented Querying)

위의 SQL 쿼리는 STATUS가 ‘SOLD OUT’인 BOOK 들의 AUTHOR 정보를 가져오는 테이블이다. JOOQ를 이용해서 자바로 변환하면, 이렇게 짤 수 있다.

JOOQ를 이용하면, SQL 문법과 상당히 유사한 자바 코드로 SQL 쿼리를 작성 할 수 있다. 자 그렇다면 이제 JOOQ를 이용해서 내가 작성해두었던 SQL SELECT 쿼리들을 자바로 옮길 차례다.

keep-calm-and-code-on
나우 잇츠 타임 포 코딩!

하나만 간단히 살펴보자. 가자고 서비스 운영에 필요한 정보 중에 특정 아이템을 구매한 고객들의 이름, 이메일, 전화번호를 알아야 하는 경우가 있었다. 이 리스트를 추출하기 위해서 아래와 같은 SQL SELECT 쿼리를 작성했었다.

이 것을 새로 생성한 Repository 클래스의 public List<FrontUser> getItemUserStat(List<String> itemCode) 메서드에 구현했다.

잠시 코드를 살펴보자. 먼저 해당하는 SQL SELECT 쿼리를 JOOQ를 이용해서 만들고 나서, fecthInto(FrontUser.class) 메서드를 호출했다. 이 메서드는 FrontUser 라는 모델 클래스의 리스트(List<FrontUser>)를 리턴하는 것을 확인 할 수 있다. 따라서 이 메서드에, 조회하고 싶은 아이템들의 itemCode를 List<String> 로 전달하면 해당하는 사용자의 정보를 FrontUser 모델 클래스의 리스트로 얻을 수 있다.

이렇게 JOOQ로 쿼리를 짜면 쉽고 간단하게 쿼리를 만들 수 있다. IDE의 자동완성 기능의 도움을 받는다면 일일이 JOOQ의 문서나 매뉴얼을 뒤져보지 않아도 그 자리에서 바로 SQL 쿼리를 자바로 옮길 수 있다.

이렇게 완성된 Repository 메서드를 이제는 거꾸로 Service에서 이용하고, 마지막으로 Controller에서 Service 메서드를 호출하면 SQL로 DB에 쿼리를 날렸을 때와 동일한 데이터를 자바 객체 묶음(Collector: List, Map 등등)으로 얻을 수 있었다. Controller의 각 메서드에 이제 RESTFul 한 URL을 할당해주기만 하면 웹 API가 완성된다!

새로운 세계

그렇게 작성한 나의 첫 백엔드 코드를 커밋(Git Commit)하고, 마침내 나의 풀리퀘스트(Pull Request)가 master 브랜치에 머지(merge) 되는 순간, 새로운 세계가 열렸다! 얕게는 매일 SQL 쿼리를 짜고 DB에서 데이터를 긁어오는 반복 노동으로부터의 해방된 것이고, 깊게는 백엔드 개발의 프로세스에 대한 경험을 해보게 된 것이다. 무엇보다 기뻤던 것은 드디어 가자고 프로젝트에 유의미한 기여를 할 수 있게 되었다는 사실이었다.

물론 이 것은 시작일 뿐이었다. 작동하는 웹 API를 만들어보기는 했지만, 웹 어플리케이션이라는 것이 무엇인지, 어떻게 동작하는지 제대로 이해하게 된 것도 아니고, Spring, JOOQ, JPA와 같은 기술들의 문서를 숙지하고 개발한 것도 아니고, 더군다나 효율적인 개발 프로세스대로 개발을 해본 것도 아니다. 그렇지만 내가 만든 API가 제대로 가자고 웹 어플리케이션 위에서 동작한 것을 확인했을 때의 즐거움이란 어마어마한 것이었다. 하하! 드디어 나도 이제 웹개발자구나! 🙂

여튼 이렇게 웹 개발의 ‘ㅇ’도 모르고 있었던 내가 DB를 다루고 관리하는 일부터 시작해서 백엔드 어플리케이션을 개발하는 백엔드 개발자의 업무를 경험해보기 까지 1달이 좀 못되는 시간이 걸렸다. 물론 그때는 몰랐다. 백엔드 개발에 더 익숙해지는데까지는 훨씬 더 많은 시간이 걸릴 것이라는 것을…

백엔드 개발 도전! 편 끝. 레저큐 인턴기는 계속 이어집니다…
By EastskyKang

AOP와 프록시 그리고 스프링 컨테이너

안녕하세요. 가자고 시니어 개발자 HJ.Park 입니다.
이번 포스트에서는 Java 진영에서 가장 많이 사용하고 있는 Spring 프레임워크에서 우리가 자연스럽게 사용하고 있지만 범하기 쉬운 실수에 대해서 공유합니다.
경험에 대해 자연스럽게 공유하기 위해서 편하게(라고 쓰고 반말이라고 읽습니다.) 쓰도록 하겠습니다.

사건의 발단은 이랬다.

가자고의 상품 유형은 매우 다양했고 그 다양성 때문에 결제로직은 매우 복잡한 비지니스 로직을 가지고 있었다.
결제 모듈을 담당한 개발자(이하 A 개발자)가 복잡한 비지니스 로직을 풀기 위해서 팩토리 패턴과 필터 패턴을 사용했는데 A 개발자는 팩토리에서 새 필터 인스턴스(new로 생성한)를 반환하도록 설계했다.
아마 그 개발자는 일부러 그렇게 설계했었던 것 같다. 처음에 해당 로직은 이상없이 잘 돌아갔다. 하지만 문제는 유지보수 및 기능을 계속 추가하면서 발생했다.
해당 모듈을 다른(이하 B 개발자) 개발자가 인수인계를 받았으나 해당 부분에 대한 유의사항(팩토리에서 반환하는 인스턴스가 스프링의 인스턴스가 아니라는 사실을…)을 전달받지 못했다.

그리고 약 6개월 후 우리는 팩토리에서 반환되는 인스턴스 필터의 메서드에 @Transactional을 걸었다…
처음에는 @Transactional이 복잡한 비지니스 로직과 구조속에 깊이 감추어져 있었기 때문에 우리의 잘못을 바로 알아채지 못했다.

시간이 지나면서 우리의 예상과 다르게 롤백된 데이터들이 발견되기 시작했다.
처음엔 그냥 하이버네이트의 트랜젝션 매니저(우리는 하이버네이트를 사용중이다.)가 이상하게 동작한다고 생각하고 하이버네이트를 욕하고 넘어갔다. 🙁

그리고 약 일주일 후 필터 인스턴스에 AOP를 걸려고 했지만 AOP가 제대로 동작하지 않았다.(@Transactional도 AOP로 동작한다.)
그래서 디버깅한 결과 필터 인스턴스가 new로 반환된 인스턴스이기 때문에 AOP가 전혀 동작하고 있지 않았던 것이다.
(스프링 프레임워크에서 AOP를 사용하기 위해서는 인스턴스에 프록시가 걸려있어야 하고 스프링 컨테이너에 등록되어 있는 인스턴스는 프록시가 걸려있다.
반면 동일한 클래스라고 해도 new로 생성한 인스턴스는 프록시가 걸려있지 않다.)

스프링의 빈을 사용하는 규칙은 간단하지만 이러한 실수는 복잡한 비지니스로직과 구조… 그리고 여러 개발자를 거치다 보면 이러한 문제는 어느 프로젝트나 발생할 수 있는 문제다.
그렇기 때문에 우리는 스프링 컨테이너와 AOP 그리고 프록시에 대한 아래 기본적인 사항을 꼭 숙지해야한다.

스프링 컨테이너

스프링을 사용한다면 스프링 컨테이너에 대한 기본적인 이해가 필요하다.
우리는 스프링의 인젝션(@Inject, @Autowire, @Resource…)을 사용하면서도 이 인스턴스들이 어디에서 오는지 별로 신경을 쓰지 않는 사람들도 많다.
그래서 스프링의 인스턴스들이 싱글턴으로 관리되고 있음을 망각하고 new를 사용하는 경우도 가끔 있다.
물론 단순한 로직이나 구조에서는 잘 일어나지는 않지만 복잡한 비지니스로직과 구조가 만나면 누구라도 충분히 할 수 있는 실수다.
스프링의 org.springframework.stereotype 패키지에는 @Component, @Controller, @Repository, @Service등의 annotation(이하 애노테이션)이 있다. 이 애노테이션을 클래스에 걸고 스프링에 해당 패키지를 스캔하도록 설정하면 해당 클래스들은 스프링이 초기화될 때 스프링 컨테이너에 하나의 인스턴스가 들어가게 된다.
물론 @Bean으로 직접 인스턴스를 생성, 설정하고 return하는 방법이나 XML로 선언하는 방법 등이 있다.

AOP와 프록시

스프링의 AOP는 대상 인스턴스에 반드시 프록시가 걸려있어야 작동을 할 수 있다.
인스턴스에 프록시가 걸리기 위해서는 스프링 컨테이너에 빈을 등록해야하며 반드시 스프링 컨테이너에 등록된 빈을 사용해야만 한다.

예제 – 스프링 컨테이너에 등록된 빈과 새로 생성한 인스턴스 비교

소스코드: https://github.com/hyunjun19/spring-boot-sample-aop

스크린샷 2016-08-23 오전 12.52.34
위 이미지를 보면 springInstanceService와 newInstanceService는 동일한 HelloWorldService 클래스의 인스턴스이지만 hashcode가 2872, 2876으로 각기 다른 인스턴스임을 알수 있다.
또한 springInstanceService를 보면 CglibAopProxy가 여러개 걸려있는 모습을 볼수있는 반면에 newInstanceService는 아무런 프록시도 걸려있지 않은 모습을 볼수 있다.