: 시스템 관점에서 프레임워크들의 책임을 분리하여 클린한 코드를 만들기 위한 방법, 사실 오픈소스 기여할 깜냥 정돈 되야 생각해볼만한 가치인듯?
도시를 세운다면?
심시티라는 게임을 안다면 한번 생각해보자.
온갖 세세한 사항들을 시장이 된 당신이 모두 관리할 수 있을까?
주택에 불이 났을때, 직접 출동 명령을 내린다던가, 범죄가 발생했을 때, 경찰을 출동시킨다던가
불가능하다. 각 책임들은 당신이 설치해둔 경찰서나 소방서의 경찰서장이나 소방서장한테 위임되고 그들이 처리한다.
그리고 이렇게 위임된 책임들은 상위 수준인 당신에게 의존성을 가진다. 당신이 경찰서를 확장한다던가, 개선, 없앨 순 있지만 범인을 잡을 순 없다는 것으로 설명할 수 있다.
: 이처럼 도시가 잘 돌아가고 있는 이유는 적절한 추상화와 모듈화를 통한 적절한 책임의 배분덕분이다.
: 현재 추상화 수준에서 맡은 바 책임을 적절히 잘 수행해내면 상위 추상화 수준이나 하위 추상화 수준에 대한 정보를 몰라도 잘 돌아가게 되어있다.
⇒ 경찰서장은 경찰서장에게 배분된 책임만 잘 수행해내면 된다. 경찰서장의 하위 추상화 수준인 범인을 잡는다던가, 상위 추상화 수준인 공공기관을 관리한다던가 그런 책임을 맡을 필요는 없다.
: 소프트웨어도 똑같다. 하나의 소프트웨어가 잘 구성된 도시 시스템처럼 동작한다면 각 객체들은 적절한 책임을 가지고 적당한 수준의 작업을 처리할 것이고 우리의 문제를 잘 해결해줄 것이다.
⇒ 소프트웨어를 만들때, 적절한 추상화 수준 부여와 모듈화를 잘해내면 마치 좋다.
시스템 제작과 시스템 사용을 분리하라
: 흔히 우리는 제작과 사용이 같은 목적을 가지고 있으리라 추측을 하곤 한다. 하지만 시스템에서 이는 명백히 다른 책임, 목적을 가지고 있음을 잊지말자.
⇒ 칼을 만들기 위해선 대장장이가 필요하지만 칼을 사용하기 위해선 요리사가 필요하다.
: 일반적으로 애플리케이션은 컴파일 - 런타임의 생명주기를 가진다. 처음엔 컴파일 : 런타임 = 제작 : 사용로 이해해보려했으나 잘 되지 않았다.
: 그래서 런타임을 두 개로 나눠서 이해해보려했더니 그럭저럭되었다. 이를 객체 생성과 객체 사용으로 이름짓고 이해해보자.
: 그렇다면 여기서 얘기하는 분리는 객체 생성과 사용을 분리하란 뜻일 것이다. 왜 그렇게 해야할까? 다음 예시를 보자.
public Service getService() {
if (service == null) {
return new MyServiceImpl(...);
}
return service;
}
JavaScript
복사
: 객체 생성해주는 과정을 적절히 분리하지 않는다면 다음과 같은 문제점들이 발생한다.
⇒ 객체 사용은 객체 생성에 의존성을 가지게 된다. 위 코드의 3번 째 줄이 바로 그 예시다.
⇒ 테스트하기가 어려워졌다. 만약 MyServiceImpl 클래스가 무거운 객체라면 당신은 테스트를 가볍게 유지하기 위해 Mock 객체를 service 필드에 할당할 것이다.
또한 일반 런타임 로직인 getService()에 객체 생성 로직이 존재하는 탓에 런타임 상황에서 service가 null인 경우와, null이 아닌 경우 모두 테스트해야 한다.
⇒ 경로가 나뉘었네? SRP를 위반했다. 근데 난 이건 크게 신경 안 쓴다.
⇒ 무엇보다 MyServiceImpl 클래스가 런타임의 모든 상황에 적합한 객체임을 보증할 수 없다.
•
그렇다면 시스템에서의 생성과 사용은 어떻게 분리되어야하는 걸까?
1.
Main 분리
: 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈에 두고 나머지 시스템은 객체가 생성되고 의존성이 연결되었다고 믿고 개발한다.
2.
Factory
: 객체가 생성되는 시점을 런타임에서 제어해야할 때 사용할 수 있다.
3.
Dependancy Injection
: IoC 기법을 의존성 관리에 적용한 방법이다.
: 한 객체의 보조 책임을 새로운 객체에게 떠넘긴다. 시장이 경찰서장한테 치안 업무를 이관하듯이
: 의존성 관리의 경우 객체는 이를 다른 전담 메커니즘에게 떠넘긴다.
확장
: 군락은 마을로, 마을은 도시로 성장한다. 그 과정에서 많은 것들이 확장되거나 변경된다.
: 처음부터 완벽하게 도시를 세울 순 없다. 돈이 매우 많다면 모르겠지만 대개 필요한 요구사항에 따라 확장을 거쳐왔다.
: 작가는 여기서 처음부터 올바르게 시스템을 만들 수 있다는 믿음은 미신이라는 표현을 사용했다. 오늘은 오늘의 일을 하고 내일은 내일의 요구사항에 맞춰 확장해나가면 된다.
: 그렇다면 확장은 소프트웨어 시스템에서 필히 일어날 문제와도 같을텐데, 이를 슬기롭게 풀어내려면 어떤 도구들의 도움을 받을 수 있을까?
: 코드 수준에서는 TDD와 Refactoring의 도움을 받을 수 있다. 하지만 시스템 수준에선? 관심사를 적절히 분리해 관리한다면 도움을 받을 수 있을 것이다.
•
EJB2 아키텍처의 예시
: EJB2는 일부 영속성(Database), 트랜잭션 동작 방식, 보안 제약조건 등은 횡단 관심사(배치 기술자에서 정의)를 통해 완벽하게 분리한다.
: 다만 비즈니스 논리가 EJB2 컨테이너 논리에 강하게 결합되어 적절한 관심사의 분리에 실패해, 점진적인 발전이 어려웠다.
: 그렇다면 영속성, 트랜잭션 동작 방식, 보안 제약 조건을 완벽하게 분리해줄 수 있었던 횡단 관심사란 무엇일까?
•
횡단 관심사란?
: 소스 코드가 아닌 배치 기술자에서 정의를 통해 관심사를 분리하는 것, 영속성이나 트랜잭션같은 관심사는 애플리케이션의 각 영역, 표현, 응용 계층들을 넘나드는 경향이 있기 때문이다.
: 영속성의 경우, 영속적으로 저장할 객체를 선언(@Entity)하면 이 객체에 대한 영속성 책임은 영속성 프레임워크에게 위임된다.
: 다음은 Java에서 횡단 관심사를 분리하기 위한 방법들이다.
◦
자바 프록시
: 대부분 JDK 동적 프록시나 CGLIB를 이용해 구현하는 자바 프록시는 단순한 상황에 적합하다.
: 인터페이스에 적용되는 프록시는 JDK 동적 프록시, 클래스는 바이트코드 라이브러리를 쓴다.
: 특정 작업에 대해서 로깅을 처리한다던가, 실행시간을 계산한다던가 등, 모든 횡단 관심사가 필요한 영역에 적용 가능하다.
: 다만 시스템 단위로 동작하여 실행 지점을 명시하는 메커니즘은 제공하지 않는다.
◦
순수 자바 AOP 프레임워크
: Spring, JBoss와 같은 순수 자바 관점을 구현하는 프레임워크는 내부적으로 프록시를 사용해 횡단 관심사를 분리하고 있다.
: 이런 특성 덕분에, 자바에서 POJO는 순수하게 도메인에 초점을 맞출 수 있으며 동시에 프레임워크에 의존하지 않는다.
: 프로그래머가 어노테이션을 사용해 명시해둔 관점을 프레임워크가 프록시나 바이트코드 라이브러리를 사용해서 구현한다.
◦
AspectJ
: 언어 차원에서 관점을 모듈화하여 구성하는 자바 언어의 확정 버전이다.
테스트 주도 시스템 아키텍처 구축
: 이상적인 시스템 구조는 각 도메인들이 POJO 객체로 구현되는 모듈화된 관심사 영역으로 구성된다.
: 애플리케이션의 관심사와 프레임워크의 관심사를 분리하면 TDD가 가능해지며 모든 것을 처음부터 설계하기 위해 많은 비용을 지불하지 않아도 된다.
의사 결정을 최적화하라
: 한 사람이 모든 결정을 내리기는 어렵다. 시장이 경찰서장 대신 결정을 내릴 순 있지만 그 결정이 합리적이기 위한 논리를 제시하기는 어렵듯이 말이다.
: 가장 좋은 의사 결정 시기는 데드라인 전까지 최대한 많은 정보가 모일 때다.
: 너무 일찍 결정해버리면 우리는 고객의 불충분한 피드백과 불충분한 고민으로 더 효율적인 구현을 찾아내기 힘들다.
: 관심사를 모듈로 잘 분리한 POJO 시스템은 기민함을 제공한다는데, 이는 확장성, 변동성을 의미하는 것 같다.
: 코드를 변경하기 좋게 구현한다면 우리는 최신 정보에 기반한 합리적인 결정을 토대로 코드를 쉽게 수정할 수 있다.
: 코드를 변경하기 좋게 구현하라기보단 관심사를 잘 분리해서 구축한 시스템은 자연스레 그렇게 된다는 의미인 것 같다.
명백한 가치가 있을 때 표준을 현명하게 사용하라
: 표준은 그 자체로 큰 의미를 가진다. 재사용성을 높여주고 정보를 찾기도 쉽다. 사실 단점보다 장점이 많다.
: 하지만 무조건 표준을 고집할 필요는 없다. 가벼운 서버를 만들어야할 때, 과한 비용을 지출해서 표준을 이용해 개발을 진행(nest.js)하기보다 가볍게 구현(express)하라
: 현명하게 사용하라는 것은 아마 비용, 고객 가치 등을 의미하는 것 같다.
시스템은 도메인 특화 언어가 필요하다.
: 노가다 현장을 가보면 라다, 가다와꾸라는 생소한 단어들을 사용한다. 이는 해당 분야에 있는 사람들이라면 쉽고 명확하게 알아들을 수 있는 그들만의 언어다.
: 최근 프로그래밍도 DSL이 조명받기 시작했다. 간단한 스크립트 언어나 표준 언어로 구현한 API를 가르키는 언어로 도메인 개념과 그 개념을 구현한 코드 간의 간극을 줄여줄 수 있다.
⇒ QueryDSL 등.. 도메인 개념 : 유저를 찾고 싶어요(findUser) → 그 개념을 구현한 코드 : Select * From User Where user_id = ?
: 이는 높은 추상화 수준을 유지할 수 있는 좋은 예로 개발자의 추상화 수준이 과도하게 낮아지지 않게 해줄 수 있다.
결론
: 코드 뿐 아니라 시스템 역시 깨끗해야 한다. 깨긋하지 못한 아키텍처는 도메인 논리에 불필요한 정보나 추상화 수준을 제공해 그 의도를 알아보기 힘들게 하며 기민성을 떨어뜨린다.
: 이렇게 되면 제품 품질이 떨어진다. 신경 쓸 것이 많아지니 버그가 발생할 확률도 높아지고 해당 부분에 대해 애플리케이션이 과의존하게 된다. 이는 곧 생산성 저하로도 이어지게 되는 것이다.
: 모든 추상화 단계에서 그 의도는 명확해야 한다. 획득할 수 있는 정보가 명확해야한다는 것이다. 그러려면 POJO를 작성하고 AOP를 이용해 부가 로직들을 분리해야한다.