"복잡성은 죽음이다. 개발로부터 생기를 앗아가며, 제품을 계획하고 기획하고 제작하고 테스트하기 어렵게 만든다."
- 레이오지, 마이크로소프트 최고 기술 책임자
1. 도시를 세운다면?
- 도시를 세운다면 혼자서는 무리이다. 각 분야를 관리하는 팀이 있기 때문에 돌아간다.
- 또한 적절한 추상화와 모듈화 때문에 잘 돌아간다.
- 그러나 막상 팀이 제작하는 시스템은 비슷한 수준으로 추상화를 이뤄내지 못한다.
- 높은 추상화를 통해 시스템 수준에서도 깨끗함을 유지하는 방법을 살펴보자.
2. 시스템의 생성과 사용을 분리하라.
- 우선 제작(Construction)은 사용(use)과 아주 다르다는 사실을 명심해야 합니다.
- 소프트웨어 시스템은 준비과정(객체생성, 의존성 연결)과 런타임 로직을 분리해야 합니다.
public Service getService() {
if (service == null)
service = new MyServiceImpl(...); // 모든 상황에 적합한 기본값일까?
}
- 위 코드와 같이 필요할 때까지 객체생성을 미루는 것을 초기화 지연 혹은 계산 지연이라고 합니다.
- 만약 무거운 객체라면 단위 테스트에서 테스트 전용 객체를 할당해야 하는 부담이 있고, 모듈성 저조와 중복을 부를 수 있으므로 자주 사용하지 않는 것이 좋습니다.
1) Main 분리란?
- 시스템에서 객체 생성과 객체 사용을 분리하는 방법입니다.
- 객체 생성과 관련된 코드는 모두 main함수나 main이 호출하는 별도 모듈에 둡니다.
- 나머지 시스템 코드(비즈니스 로직)은 "이미 모든 객체가 생성되어 있고, 의존성이 연결된 상태"라고 가정합니다.
2) 팩토리
- 객체를 언제 만들지(생성 시점)는 애플리케이션이 결정합니다.
- main에서 팩토리 객체를 만들어 필요한 곳에 전달합니다.
- 만약 구체적인 생성 방법을 숨기고 싶으면 Abstract Factory 패턴을 사용합니다.
- 즉, "생성 시점은 애플리케이션이 알지만, 생성 방법(코드)은 모릅니다."
3) 의존성 주입(Dependency Injection, DI)
- 객체의 사용과 생성을 완전히 분리하는 강력한 방법 입니다.
- 의존성 주입은 제어의 역전(IoC) 기법의 한 종류 입니다.
- IoC(제어의 역전)란, 객체가 스스로 필요한 객체를 만들거나 찾지 않고, 외부에서 필요한 객체를 주입받는 방식
- 이렇게 하면 각 객체는 "넘겨받은 책임"만 수행하고, 단일 책임 원칙을 지킬 수 있습니다.
// 강하게 결합된 코드 (Strongly Coupled)
public class Wheel {
private Tire myTire;
public Wheel() {
this.myTire = new Tire(); // Wheel이 직접 Tire를 만듦
}
}
// 느슨하게 결합된 코드 (Loosely Coupled, IoC 적용)
public class IocWheel {
private Tire myTire;
public IoCWheel(Tire tire) {
this.myTire = tire; // 외부에서 Tire를 주입 받음
}
}
3. 확장
- 군락은 마을로, 마을은 도시로 성장합니다. 성장에는 고통이 따릅니다.
- 처음부터 올바르게 시스템을 만들 수는 없지만 주어진 스토리에 맞춰 시스템을 구현하고 확장하면 됩니다.
- 이것이 반복적이고, 점진적인 애자일 방식의 핵심입니다.
- TDD와 리팩토링으로 얻어지는 깨끗한 코드는 코드 수준에서 시스템을 조정하고 확장하기 쉽게 만듭니다.
소프트웨어 시스템은 물리적인 시스템과 다릅니다. 관심사를 적절히 분리해 관리한다면 S/W 아키텍처는 점진적으로 발전할 수 있습니다. (AOP)
4. 관점지향 프로그래밍 (AOP)
- 자바 프록시는 객체의 메서드 호출을 가로채서 부가적인 작업을 쉽게 추가할 수 있는 기술입니다. 간단한 상황에서 개별 객체나 클래스의 메서드 호출을 감싸고 싶을 때 유용합니다.
- 이 때 프록시(Proxy)를 활용할 수 있는데, 프록시는 "대리" 또는 "대리인"이라는 뜻으로, 어떤 객체에 직접 접근하지 않고 중간에 대리 역할을 하는 객체나 서버를 의미합니다. 즉, 프록시 객체는 진짜 객체를 대신해서 클라이언트 요청을 받아 처리하거나, 진짜 객체에게 요청을 전달하는 역할을 합니다.
- 프록시는 접근 제어(보안, 인증), 부가 기능 추가(로깅, 캐싱 등), 객체에 대한 접근을 통제하고, 원본 객체의 변경 없이 기능 확장이 가능합니다.
1) JDK 프록시 예제
Bank bank = (Bank) Proxy.newProxyInstance(
Bank.class.getClassLoader(),
new Class[] { Bank.class },
new BankProxyHandler(new BankImpl())
);
- 위 코드는 Bank 인터페이스를 구현한 프록시 객체를 생성합니다.
- 실제 메서드 호출은 BankProxyHandler의 invoke() 메서드에서 가로채서 처리합니다.
2) 프록시와 AOP 프레임워크
<bean
id="bank"
class="com.example.banking.model.Bank"
p:dataAccessObject-ref="bankDataAccessObject"
/>
- 위와 같이 XML로 의존성을 선언하면, 실제 자바 코드에서는 스프링 관련 코드가 거의 필요 없습니다.
- DI 컨테이너가 객체 생성과 의존성 주입, 프록시 적용을 모두 처리합니다.
3) AspectJ
- AspectJ는 자바에서 AOP를 구현하는 대표적인 프레임워크 입니다. 로깅, 트랜잭션, 보안 등을 로직과 분리해서 관리할 수 있도록 합니다.
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Method called: " + joinPoint.getSignature().getName());
}
}
- @Aspect : 이 클래스가 Aspect임을 나타냅니다.
- @Before : 지정한 메서드 실행 전에 log.Before 메서드가 실행됩니다.
- "execution(* com.example.service.*.*(..))" : com.example.service 패키지의 모든 메서드에 적용됩니다.
- JoinPoint : 실행되는 메서드의 정보를 담고 있습니다.
5. 테스트 주도 시스템 아키텍처 구축
- 관점으로 관심사를 분리하는 방식은 막강하다. 코드 수준에서 아키텍처 관심사를 분리할 수 있다면 진정한 테스트 주도 아키텍처 구축이 가능해진다.
- 그때그때 새로운 기술을 채택해 단순한 아키텍처를 복잡한 아키텍처로 키워할 수도 있지만, 관점을 효과적으로 분리한다면, 단순하면서도 멋지게 분리된 아키텍처로 기반 구조를 조금씩 확장해갈 수 있습니다.
6. 의사 결정을 최적화하라.
- 모듈을 나누고 관심사를 분리하면 지엽적인 관리와 결정이 가능해집니다.
- 최대한 정보를 모아 최선의 결정을 내리고, 관심사를 잘 분리한 POJO 시스템은 기민하게 작용할 수 있습니다.
7. 명백한 가치가 있을 때 표준을 현명하게 사용하라.
- 표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 적절한 경험을 가진 사람을 구하기 쉬우며, 좋은 아이디어를 캡슐화하기 쉽고, 컴포넌트를 엮기 쉽다.
- 하지만 때로는 표준을 만드는데 시간이 너무 오래 걸려 업계가 기다리지 못합니다. 간단한 프로젝트에도 표준에 집착하지는 말자.
8. 시스템은 도메인 특화 언어가 필요하다.
- 소프트웨어 분야에서도 DSL이 조명 받기 시작했습니다. DSL(Domain-Specific Language, 도메인 특화 언어)란? 특정 분야(도메인)의 문제를 쉽게 해결할 수 있도록 고안된 그 분야에 특화된 언어 입니다. (SQL, 정규표현식, HTML, CSS 등)
- 좋은 DSL은 도메인 개념과 그 개념을 구현한 코드 사이에 존재하는 '의사소통 간극'을 줄여줍니다.
- 효과적으로 사용한다면 DSL은 추상화 수준을 코드 관용구나 디자인 패턴 이상으로 끌어올릴 수 있습니다.
범용 언어(General Purpose Language, GPL) : 자바, 파이썬, C 등 모든 분야에 쓸 수 있는 언어 입니다.
결론
- 시스템 역시 깨끗해야 합니다. 도메인 논리를 흐리고 기민성을 떨어뜨리면 제품 품질이 떨어집니다.
- 모든 추상화 단계에서 의도는 명확히 표현해야 합니다. 그러려면 POJO를 작성하고, 관점 혹은 관점과 유사한 메커니즘을 사용해 각 구현 관심사를 분리해야 합니다.
728x90
'Software Tech > Spring (feat.JAVA)' 카테고리의 다른 글
[클린코드] 8. 동시성 (2) | 2025.05.20 |
---|---|
[클린코드] 7. 창발성 (0) | 2025.05.20 |
[클린코드] 5. 클래스 (1) | 2025.05.20 |
[클린코드] 4. 경계 & 단위테스트 (2) | 2025.05.19 |
[클린코드] 3. 예외처리 (0) | 2025.05.19 |