헥사고날 아키텍처를 처음 접해보았을 때는 인턴때였습니다. 프로젝트 베이스가 깔려지고 사수님한테 설명을 듣고 "나도 저런 아키텍처를 내 코드에 사용해보고싶다.. 더 나아가서 아키텍처를 개발해보고 싶다" 라는 생각이 들었습니다. 마침F-lab을 하며 멘토님이 헥사고날 아키텍처를 사용하여 코드 작성하는 모습을 보여주었습니다. 인턴 때 예습이 되어있어서 그런지 정말정말 이해가 잘 되는 느낌이었습니다.
헥사고날 아키텍처
내부 영역- 순수한 비즈니스 로직을 표현하며 캡슐화된 영역이고 기능적 요구사항에 따라 먼저 설계
내/외부를 분리하고 내부에는 비즈니스 로직들만 존재하고 외부에는 내부의 비즈니스 로직의 관심사가 아닌 것들(database, logging, etc...)이 존재하였습니다. 내/외부를 adaptor와 port를 이용하여 내부의 비즈니스 로직이 필요한 것들을 꽂아서 사용할 수 있는 구조였습니다. 다음 그림으로 설명이 될 것 같습니다.
USB를 옆의 그림처럼 USB 포트에 꼽아서 사용하는 것과 같다고 생각합니다. USB는 우리가 원하는 것들을 꼽아서 사용하지 않나요? 그런거와 같이 우리가 비즈니스로직에 MySql을 사용하고싶으면 MySql에 해당하는 어뎁터를 포트에 꼽고 H2를 사용하고 싶으면 그에 해당하는 어뎁터를 꼽아 사용할 수 있습니다.
어뎁터는 포트에다가 꽂을 수 있는 것들을 골라서 꼽을 수 있습니다. 비즈니스 로직에 사용하고 싶은 어뎁터로 포트에 꼽으면 되는 구조였습니다.
어떻게?
인터페이스가 가능하게 해줍니다.
원래 이런 구조였습니다. CommandHandler가 바로 MybatisRepositoryRepository를 의존하고 있었습니다. 그런데 사실상 CommandHandler는 MyBatis, JPA, JDBCtemplate 등 무슨 ORM을 사용하는지 비즈니스 로직의 관심사가 아닙니다. 그리고 바로 구현 객체를 의존하면 변경에 유연하지 않다고 생각합니다. 이런 상황을 풀어줄 친구는 인터페이스 입니다. 다음 사진으로 가보겠습니다.
이렇게 인터페이스인 IBookRepository를 의존하는 방법입니다. 여기서 인터페이스는 port 역할을 합니다. 그리고 BookMybatisRepository는 adaptor 역할을 합니다.
확실하게 관심사가 분리될 수 있었습니다. 비즈니스 로직은 비즈니스로직이 중심이 되고 비즈니스 로직이 필요한 것들은 포트를 게이트로 삼아 끼워넣는 방식은 변경에 유연하다고 생각했습니다.
바꾼 이유는 공부 목적도 있었고 멘토님이 말하시길 "코드상 눈에 보이는 표식을 하지 않았기 때문에 모르는 사람이 볼때는 Aspect 로직이 왜 실행되는지 이해를 하지 못하는 경우도 많다"고 하셨다. 듣고 보니 정말 그럴 것 같았다.
어떻게 바뀌었냐면
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {}
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Around("@annotation(Logging)") // Custom AOP
public Object logging(ProceedingJoinPoint pjp) throws Throwable {
// ... 이하 위 로직과 같음
이렇게 바꿈으로써 전에 없었던 어노테이션이 생겼고, 그 어노테이션으로 무엇을 하려고 하는지 정확히 알 수 있었습니다.
이 과정에서 @Transactional, @Cacheable, @Async 에너테이션도 AOP가 적용된 사례라는 것을 알게되었습니다.
name: PR test CI
# 하기 내용에 해당하는 이벤트 발생 시 github action 동작
on:
push: # feature/*와 pre-production와 main 브랜치에서 push가 일어났을 때 github action 동작
branches:
- 'feature/*'
- 'main'
- 'pre-production'
pull_request: # feature/*와 pre-production와 main 브랜치에서 PR이 일어났을 때 github action 동작
branches:
- 'feature/*'
- 'main'
- 'pre-production'
# 참고사항
# push가 일어난 브랜치에 PR이 존재하면, push에 대한 이벤트와 PR에 대한 이벤트 모두 발생합니다.
jobs:
build:
runs-on: ubuntu-latest # 실행 환경 지정
steps:
- uses: actions/checkout@v2 # github action 버전 지정(major version)
- name: Set up JDK 11 # JAVA 버전 지정
uses: actions/setup-java@v3
with:
java-version: 11
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
run: ./gradlew build
# run: ./gradlew build -x test
# - name: Test with Gradle # test application build
# run: ./gradlew test
- name: Publish Unit Test Results # test 후 result를 보기 위해 추가
uses: EnricoMi/publish-unit-test-result-action@v2
if: ${{ always() }} # test가 실패해도 report를 남기기 위해 설정
with:
files: build/test-results/**/*.xml
그런데....
이 화면의 연속이었다. 어제 오늘...순수 해결하려고 노력한 시간만 대략 20시간 동안 꼬박 이것만 한 것 같다.
DoseoroApplicationTests > contextLoads() FAILED java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
이 오류에 대한 원인을 살펴봤었다.
MySql의 문제
DB가 물리지 않았기 때문
디비가 물렸어도 테이블이 없기 때문
현재 MyBatis를 사용하고 있는데 JPA나 Node js에 Sequelize 같은 ORM은 자동으로 테이블을 만들어 주지만 MyBatis에는 그런게 없는데 어떡하지? 의 문제
등등 긴 시간동안 많은 고민이 있었던 것 같다...
1. 처음에 MySql의 문제로 다가갔다.
테스트 코드가 돌려면 어쨌든 DB에 갔다 와야하는데 깃헙 액션 스크립트를 봤는데 MySql을 세팅하는 로직이 없었다. 그래서
- name: Setup MySQL uses: samin/mysql-action@v1 with: mysql user: 'root' mysql password: ${{ secrets.MYSQL_PASSWORD }}
이 코드를 추가했다. 그런데 예상 외로 실행되지 않았다... 그렇다면 무슨 문제가 있을까 하며 다른 문제를 찾았다. MySql을 세팅을 했지만 거기에 테이블이 없네??? 그러면 만들어야지. 라며 생각했다. 그런데 H2가 요즘 현업에서 테스트용도로만 쓴다고 했다.
2. H2의 적용
그래서 찾아보니 H2는 정말정말 구미가 당길만한 능력이 있는 친구였다. In-Memory 형식 디비를 구축할 수 있었다. H2 DB를 메인 메모리(JVM) 위에서 돌아가며 애플리케이션이 종료되면 없어지는 휘발성 디비였다. 그러면 완전 좋은거네??? 이득!! 하면서 바로 적용해보기로 했다. 그리고 메인 메모리에서 돌아가는 DB라 데이터 캐싱 DB로도 사용할 수 있는 장점을 느꼈다. 다시 사용할거라는 기약을 했다.
테스팅 용으로 H2를 적용은 했는데 이제 어떻게해?? 라는 의문점만 남았다. 그냥 돌려봐야지. 라는 생각만 들었다. 빨리 결과를 도출하고싶은 급한 마음이었다... 당연히 안될게 뻔했다. 내 자신이 부끄러울정도로 멍청하였다. 테이블이 없는데 어떻게 돌아가겠니!!!... 바로 테이블을 생성하는 시도로 넘어가자.
3. 테이블 생성
지금까지 MyBatis를 사용하면서 코드상으로 DDL을 한번도 한적이 없었다. 어떻게 하지..? 라는 생각이 들고 계속 검색만 했다. 정말 여러 방법들이 있었다. 이번에 자바 스프링을 처음 배우면서 MyBatis만 사용해봤으니 다른 것도 사용해볼까? 라는 생각이 들었다. JdbcTemplate이 pure 한 쿼리문을 넣을 수 있고 예전부터 사용해보고 싶었는데 이번 기회에 사용해보면 좋겠다 싶어서 사용해봤다. 정말정말 편했다...
JdbcTemplate으로 테이블을 생성하고 테스트코드 로직에 맞게 잘 수정한 뒤 로컬에서 테스트를 돌려보았다. 잘 돌아갔다. 그러면 깃헙 액션에서도 잘 돌아가겠지?? 라는 안일한 생각으로 기쁜 마음으로 돌려보았다. 결과는 아직이었다.. 계속 위와 같은 오류를 뿜어냈다. 그래도 고지가 코앞이라는 생각이 들었다. 이 문제를 해결할 자신이 있었다.
4. application.yml 의 분리
테스트 용도랑 로컬용도로 분리하였다. main과 test 폴더 각각 resource 폴더가 있고 그 안에는 application.yml 파일이 각각 있었다. 그런데 main에는 application-test.yml을 추가하고
spring:
profiles:
active: test
을 추가하며 test 용도로 분리했다. 그리고 test 폴더의 application.yml은
spring:
profiles:
active: test
config:
import: application-test.yml
main의application.yml을 불러오는 식으로 처리했다. 이제 완벽히 세팅이 끝났다고 생각하고 깃헙 액션을 돌려보았다. 잘 안됐다. 다 됐다고 생각했는데 왜 안되지???
+ ./bin에 있는 H2 라이브러리를 추가할 때 바로 위와 같은 상황일 때 main에만 추가 해주면 된다.
5. 커밋 히스토리를 보고 .gitignore를 확인하는 순간 해결
커밋 히스토리를 유심히 보니 테스트 폴더쪽에 application.yml이 들어가있지 않았다 .gitignore에 application.yml이 추가가 되어있었다. 정말 바보같은 실수였다. 테스트 폴더쪽의 application.yml과 함께 푸시를 하면서 모든 문제는 해결이 되었다. 안도의 한숨을 쉰다.
분명 @Builder 패턴으로 객체를 생성하면 되는데 왜 굳이 기본생성자가 필요한지 궁금했다.
Mybatis에서 resultType, resultMapping은 ObjectFactory라는 것을 사용하고 있다고 한다. 이 과정에서 객체의 초기화가 필요한데, 이 때 기본생성자가 먼저 호출되어 사용되어진다. 기본 생성자를 이용하여 오브젝트를 생성 한 후에는, 리플렉션을 사용하여 객체에 데이터를 바인딩 한다고 한다.
그래서 리플랙션이랑 기본생성자랑 무슨 관계가 있는데?? 그리고 Mybatis랑 무슨 관계가 있는데?
리플랙션에서 객체를 만들 때 기본 생성자를 먼저 찾는다고한다. 왜냐하면 먼저 객체를 생성해야 필드값들을 가져와서 필드값에 값을 넣어줄 수 있지않는가?? 객체를 만들 때 필요한 것이 기본 생성자이기 때문이다. 그러므로 Mybatis 로 select 하고 select 된 값들로 객체를 생성해야 하는데, 생성하는 과정에서 리플랙션(reflection)이 기본 생성자를 가르켜 객체를 생성한 후, 객체의 필드에 접근하여 값을 저장합니다.