[클린코드 완독스터디] TIL (2022.02.16)

yourjin·2022년 2월 27일
0

read.log

목록 보기
26/37
post-thumbnail

TIL (2022.02.16)

DAY 3

🔖 오늘 읽은 범위 : 11장, 시스템


😃 책에서 기억하고 싶은 내용을 써보세요.

자바에서 사용하는 관점 혹은 관점과 유사한 메커니즘 세 개를 살펴보자!

  • 자바 프록시
    • 자바 프록시는 단순한 상황에 적합하다. 개별 객체나 클래스에서 메서드 호출을 감싸는 경우가 좋은 예다.
    • 하지만 JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다. 클래스 프록시를 사용하려면 CGLIB, ASM, Javassist10 등과 같은 바이트 코드 처리 라이브러리가 필요하다
    • JDK 프록시 예제
      // Bank.java (패키지 이름을 감춘다)
      import java.util.*;
      
      // 은행 추상화
      public interface Bank {
      	Collection<Account> getAccounts(); 
      	void setAccounts(Collection<Account> accounts);
      }
      
      // Bankimpl.java
      import java.util.*;
      
      // 추상화를 위한 POJO("Plain Old Java Object") 구현
      public class Bankimpl implements Bank {
      	List<Account> accounts;
      	
      	private Collection<Account> getAccounts() { 
      		return accounts;
      	}
      	
      	public void setAccounts(Collection<Account> accounts) {
      		this.accounts = new Arraylist<Account>();
      		for (Account account: accounts) {
      			this.accounts.add(account);
      		}
      	}
      }
      
      // BankProxyHandler.java
      import java.lang.reflect.*;
      import java.util.*;
      
      // 프록시 API가 필요한 "InvocationHandler"
      public class BankProxyHandler implements InvocationHandler {
      	private Bank bank;
      	
      	public BankProxyHandler (Bank bank) {
      		this.bank = bank;
      	}
      	
      	// InvocationHandler에 정의된 매서드
      	public Object invoke(Object proxy, Method method, Object [] args)
      			throws Throwable {
      		String methodName = method.getName();
      		if(methodName.equals("getAccounts")) {
      			bank.setAccounts(getAccountsFromDatabase());
      			return bank.getAccounts( ) ;
      		} else if (methodName.equals("setAccounts")) {
      			bank.setAccounts((Collection<Account>) args[0]);
      			setAccountsToDatabase(bank.getAccounts());
      			return null;
      		} else {
      			...
      		}
      	}
      	
      	//세부사항은 여기에 이어진다.
      	protected Collection<Account> getAccountsFromDatabase() {}
      	protected void setAccountsToDatabase{Collection<Account> accounts) {} 
      }
      
      // 다른 곳에 위치하는 코드
      Bank bank = {Bank) Proxy.newProxyInstance{
      	Bank.class.getClassloader(),
      	new Class[] { Bank.class },
      	new BankProxyHandler(new Bankimpl()));
      • 프록시 API에는 InvocationHandler를 넘겨 줘야 한다. 넘긴 InvocationHandler는 프록시에 호출되는 `Bank`` 메서드를 구현하는 데 사용된다.
      • BankProxyHandler는 자바 리플렉션 API를 사용해 제네릭스 메서드를 상용하는 Banklmpl 메서드로 매핑한다.
    • 코드 ‘양'과 크기는 프록시의 두가지 단점이다. 다시 말해서, 프록시를 사용하면 깨끗한 코드를 작성하기 어렵다!
    • 또한 프록시는 (진정한 AOP 해법 에 필요한) 시스템 단위로 실행 ‘지점' 을 명시하는 메커니즘도 제공하지 않는다.
      • 흔히 (메서드 가로채기, 프록시를 통한 감싸기 등) AOP를 구현하는 기법과 AOP 자체를 혼동한다. AOP 시스템의 진정한 가치는 시스템 동작을 간결하고 모듈화된 방식으로 명시하는 능력이다.
  • 순수 자바 AOP 프레임워크
    • 다행스럽게도 대부분의 프록시 코드는 판박이라 도구로 자동화할 수 있다. 순수 자바 관점을 구현하는 스프링 AOP, JBoss AOP 등과 같은 여러 자바 프레임워크는 내부적으로 프록시를 사용한다.
      • 스프링은 비즈니스 논리를 POJO로 구현한다. POJO는 순수하게 도메인에 초점을 맞춘다. (...) 따라서 테스트가 개념적으로 더 쉽고 간단하다.
    • 프로그래머는 설정 파일이나 API를 사용해 필수적인 애플리케이션 기반 구조를 구현한다. 여기에는 영속성, 트랜잭션, 보안, 캐시, 장애 조치 등과 같은 횡단 관심사도 포함된다.
    • 이때 프레임워크는 사용자가 모르게 프록시나 바이트코드 라이브러리를 사용해 이를 구현한다. 이런 선언들이 요청에 따라 주요 객체를 생성하고 서로 연결하는 등 DI 컨테이너의 구체적인 동작을 제어한다.
    • 각 ‘빈’은 중첩된 ‘러시아 인형’의 일부분과 같다. Bank 도메인 객체는 자료 접근자 객체(Data Accessor Object, DAO)로 프록시 되었으며, 자료 접근자 객체는 JDBC 드라이버 자료 소스로 프록시 되었다. 클라이언트는 Bank 객체에서 getAccounts()를 호출한다고 믿지만 실제로는 Bank POJO의 기본 동작을 확장한 중첨 DECORATOR 객체 집합의 가장 외곽과 통신한다.
    • 애플리케이션에서 DI 컨테이너에게 (XML 파일에 명시된) 시스템 내 최상위 객체를 요청하려면 다음 코드가 필요하다.
      XmlBeanFactory bf =
      	new XmlBeanFactory(new ClassPathResource("app.xml", getClass())) ;
      Bank bank = (Bank)bf.getBean("bank") ;
      스프링 관련 자바 코드가 거의 필요 없으므로 애플리케이션은 사실상 스프링과 독립적이다.
    • EBJ3 Bank EJB (Bank 객체 개선한 코드)
      package com.example.banking.model;
      import javax.persistence.*;
      import java.util.Arraylist;
      import java.util.Collection;
      
      @Entity
      @Table(name = "BANKS")
      public class Bank implements java.io.Serializable {
      	@Id @GeneratedValue(strategy=GenerationType.AUTO)
      	private int id;
      
      	@Embeddable // Bank의 데이터베이스 행에 '인라인으로 포함된’ 객체
      		public class Address {
      		protected String streetAddr1;
      		protected String streetAddr2;
      		protected String city;
      		protected String state;
      		protected String zipCode;
      	}
      	
      	@Embedded
      	private Address address;
      	@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER,
      			mappedBy="bank")
      	private Collection<Account> accounts = new ArrayList<Account>() ;
      	
      	public int getId(){
      		return id;
      	}
      
      	public void setId(int id) {
      		this. id = id;
      	}
      
      	public void addAccount(Account account) {
      		account.setBank(this) ;
      		accounts.add(account);
      	}
      
      	public Collection<Account> getAccounts(){
      		return accounts;
      	}
      	
      	public void setAccounts(Collection<Account> accounts){
      		this.accounts = accounts;
      	}
      }
      • 일부 상세한 엔티티 정보는 애너테이션에 포함되어 그대로 남아있지만, 모든 정보가 애너테이션 속에 있으므로 코드 자체는 깔끔하고 깨끗하다.
  • AspectJ 관점
    • AspectJ는 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장이다.
    • 스프링 AOP와 JBoss AOP가 제공하는 순수 자바 방식은 관점이 필요한 상황 중 80~90%에 충분하다. AspectJ는 관점을 분리하는 강력하고 풍부한 도구 집합을 제공하긴 하지만, 새 도구를 사용하고 새 언어 문법과 사용법을 익혀야 한다는 단점이 있다.
    • 최근에 나온 Aspect] '애너테이션 폼’은 새로운 도구와 새로운 언어라는 부담을 어느정도 완화한다. 애너테이션 폼은 순수한 자바 코드에 자바5 애너테이션을 사용해 관점을 정의한다.

  • 테스트 주도 시스템 아키텍처 구성
    • 애플리케이션 도메인 논리를 POJO로 작성할 수 있다면, 즉 코드 수준에서 아키텍처 관심사를 분리할 수 있다면, 진정한 테스트 주도 아키텍처 구축이 가능해진다.

    • BDUF(Big Design Up Front) 를 추구할 필요가 없다. 실제로 BDUF는 해롭기까지 하다. 처음에 쏟아 부은 노력을 버리지 않으려는 심리적 저항으로 인해, 그리고 처음에 선택한 아키텍처가 향후 사고 방식에 미치는 영향으로 인해, 변경을 쉽사리 수용하지 못하는 탓이다.

      • 선행 설계(up-front design)라는 우수한 설계 기법과 혼동하지 않도록 주의한다. BDUF는 구현을 시작하기 전에 앞으로 벌어질 모든 사항을 설계하는 기법이다.
    • 소프트웨어 역시 나름대로 형체(physics)가 있지만, 소프트웨어 구조가 관점을 효과적으로 분리한다면, 극적인 변화가 경제적으로 가능하다. 다시 말해, ‘아주 단순하면서도’ 멋지게 분리된 아키텍처로 소프트웨어 프로젝트를 진행해 결과물을 재빨리 출시한 후, 기반 구조를 추가하며 조금씩 확장해 나가도 괜찮다는 말이다.

    • 그렇다고 ‘아무 방향 없이’ 프로젝트에 뛰어들어도 좋다는 소리는 아니다. 프로젝트를 시작할 때는 일반적인 범위, 목표, 일정은 물론이고 결과로 내놓을 시스템의 일반적인 구조도 생각해야 한다. 하지만 변하는 환경에 대처해 진로를 변경할 능력도 반드시 유지해야 한다.

      최선의 시스템 구조는 각기 POJO(또는 다른) 객체로 구현되는 모듈화된 관심사 영역(도메인)으로 구성된다. 이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합한다. 이런 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있다.

  • 의사 결정을 최적화하라
    • 모듈을 나누고 관심사를 분리하면 지엽적인 관리와 결정이 가능해진다. 도시든 소프트웨어 프로젝트든, 아주 큰 시스템에서는 한 사람이 모든 결정을 내리기 어렵다.

    • 우리는 때때로 가능한 마지막 순간까지 결정을 미루는 방법이 최선이라는 사실을 까먹곤 한다. 게으르거나 무책임해서가 아니다. 최대한 정보를 모아 최선의 결정을 내리기 위해서다.

      관심사를 모듈로 분리한 POJO 시스템은 기민함을 제공한다. 이런 기민함 덕택에 최신 정보에 기반해 최선의 시점에 최적의 결정을 내리기가 쉬워진다. 또한 결정의 복잡성도 줄어든다.

  • 명백한 가치가 있을 때 표준을 현명하게 사용하라

    표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 적절한 경험을 가진 사람을 구하기 쉬우며, 좋은 아이디어를 캡슐화하기 쉽고, 컴포넌트를 엮기 쉽다. 하지만 때로는 표준을 만드는 시간이 너무 오래 걸려 업계가 기다리지 못한다. 어떤 표준은 원래 표준을 제정한 목적을 잊어버리기도 한다.

  • 시스템은 도메인 특화 언어가 필요하다
    • DSL(Domain Specific Language)은 간단한 스크립트 언어나 표준 언어로 구사한 API를 가리킨다.

    • 좋은 DSL은 도메인 개념과 그 개념을 구현한 코드 사이에 존재하는 ‘의사소통 간극’을 줄여준다.(…) 도메인 전문가가 사용하는 언어로 도메인 논리를 구현하면 도메인을 잘못 구현할 가능성이 줄어든다.

      도메인 특화 언어(Domain-Specific Language, DSL)을 사용하면 고차원 정책에서 저차원 세부사항에 이르기까지 모든 추상화 수준과 모든 도메인을 POJO로 표현할 수 잇다.

  • 결론
    • 시스템 역시 깨끗해야 한다.
    • 모든 추상화 단계에서 의도는 명확히 표현해야 한다. 그러러면 POJO를 작성하고 관점 혹은 관점과 유사한 매커니즘을 사용해 각 구현 관심사를 분리해야 한다.
    • 시스템을 설계하든 개별 모듈을 설계하든, 실제로 돌아가는 가장 단순한 수단을 사용해야 한다는 사실을 명심하자

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요

도시든 소프트웨어 프로젝트든, 아주 큰 시스템에서는 한 사람이 모든 결정을 내리기 어렵다. (p.211)

  • 위의 구절이 이번 장에서 말하고자 했던 모든 내용의 목적이라고 생각한다. 농업, 제조업도 성장하면서 점점 분업화되며 생산성을 높였듯이, 소프트웨어 역시 더 이상 혼자서는 할 수 없을 정도로 그 규모가 커졌다. 즉, 소프트웨어에도 분업화가 필요해졌다는 의미다. 그를 쉬운 말로 표현한게 바로 관심사의 분리라고 생각한다. 서로의 목적을 분리하여 언제 어떤 변화에도 유연하게 대처할 수 있게 하는 것이다. 이번 장에서 이를 위한 다양한 기법들을 배웠다. 이런 기술들은 한번 읽는다고 익숙해지지는 않지만, 적어도 AOP(Aspect Oriented Programming)의 개념 만큼은 여기서 확실히 할 수 있을 것 같다.

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 자바 프록시 / 동적 프록시
    • 프록시란 '대리’라는 의미로 프록시에게 어떤 일을 대신 시키는 것이다.

    • 프록시 서버

      • 보안상의 이유로 서버를 외부에 노출시키지 않기 위해 서버와 클라이언트단 중간에서 접점을 담당하는 서버를 보고 프록시 서버라고 부른다.
    • 프록시 패턴

      • 객체가 들어갈 자리에 대리자 객체를 대신 투입하여, 클라이언트는 어떤 객체로부터 메소드를 호출해서 반환 값을 받는지 모르게 하는 것을 말한다.
      • Proxy 클래스가 RealSubject의 request()를 대신 호출한다.
      • 프록시 패턴을 사용하는 이유
        1. 이렇게 하면 구현 클래스에 직접 접근하지 않고, 프록시를 통해 우회해서 접근하여 흐름제어를 할 수 있다.

          • 여러 클라이언트가 동시에 직접 DB에 요청을 하면, DB에 엄청난 부하를 줄 수 있다. 하지만 Proxy를 거쳐가게 하면 클라이언트간에 요청 흐름을 제어할 수 있다.
        2. 실제 메소드가 호출되기 이전에 필요한 기능(전처리 등의)을 구현 객체 변경 없이 추가할 수 있다. (코드변경의 최소화, DECORATOR 패턴)

        3. 캐시를 사용할 수 있다. (프록시가 내부캐시를 통해 데이터가 캐시에 존재하지 않는 경우에만 주체 클래스에서 작업이 실행되도록 할 수 있다.)

    • 동적 프록시

      • 프록시를 사용하기 위해서는 대상 클래스 수만큼의 프록시 클래스를 만들어줘야 하고, 그 안에 들어가는 반복되는 코드 때문에 코드 중복이라는 단점이 있다.

      • 이러한 단점들을 보완하여 컴파일 시점이 아닌, 런타임 시점에 프록시 클래스를 만들어주는 방식이 동적 프록시이다.

      • 참고 링크: [Java] 프록시패턴 (Proxy Pattern)

      • 참고 링크: [Java] 동적 프록시(Dynamic Proxy)

  • AOP(Aspect-Oriented Programming)
    • OOP(Object-Oridented Programming)의 한계
      • 객체지향 프로그래밍은 어플리케이션을 설계할 때 책임관심사에 따라 클래스를 분리합니다.
      • 그러나 한 가지 아쉬운 점은, 여러 클래스에 로깅이나 보안 및 트랜잭션 등 공통된 기능들이 흩어져 존재한다는 점입니다. 이렇게 어플리케이션 전반에 걸쳐 흩어져 있는 공통되는 부가 기능들을 관심사(Aspect)라고 합니다.
    • AOP(Aspect-Oriented Programming)
      • 관점 지향 프로그래밍이란 OOP로 독립적으로 분리하기 어려운 부가 기능을 모듈화하는 방식입니다.
      • AOP는 핵심 비즈니스 로직과 부가 기능 Aspect를 분리하는 등 OOP를 보완하는 역할입니다.
      • 구현 방법
        1. Spring AOP를 활용한다.
        2. 프록시를 사용함으로써 부가 기능을 실행합니다.
        3. AspectJ를 사용한다.
    • 참고 링크: AOP 입문자를 위한 개념 이해하기
  • POJO(Plain Old Java Object)
    • 특정 '기술'에 종속되어 동작하는 것이 아닌 순수한 자바 객체
    • 왜 POJO를 지향해야 하는가?
      • 특정 기술과 환경에 종속되어 의존하게 된 자바 코드는 가독성이 떨어져 유지 보수에 어려움이 생겼다. 또한, 특정 기술의 클래스를 상속받거나, 직접 의존하게 되어 확장성이 매우 떨어지는 단점이 있었다. → 객체 지향 설계의 장점들을 잃어버리게 되었다!
      • 따라서 본래 자바의 장점을 살리는 POJO를 지향하게 되었다.
    • POJO를 유지하면서 특정 기술을 사용하려면? (스프링의 PSA, Portable Service Abstraction)
      • 스프링은 JPA를 표준 인터페이스로 정하면서 그 문제를 해결했다. 즉, ORM 기술을 도입하기 위한 인터페이스를 정의하고, 모든 ORM 기술들은 이 인터페이스 아래서 구현되고 실행된다.

        PSA에 대해서는 한번에 이해되지 않아, 다음에 한번 더 보면 좋겠다!

    • 참고 링크: POJO - (Plain Old Java Object)란 뭘까?
    • 참고 링크: [Spring] Spring의 핵심기술 PSA - 개념과 원리
  • DSL(Domain Specific Language)
    • 특정 도메인(산업, 분야등 특정 영역)에 특화된 언어

      아래 링크의 “우리 주변에 있는 DSL”을 보면 조금 이해가 쉬워진다. 그래도 그 중 많은 것을 아직 써보지 못해 확실하게 이해가 되지는 않는 것 같다..

    • 참고 링크: DSL(Domain Specific Language) 이해하기

소감 3줄 요약

  • 모든 추상화 단계에서 의도는 명확히 표현해야 한다. 그러러면 POJO를 작성하고 관점 혹은 관점과 유사한 매커니즘을 사용해 각 구현 관심사를 분리해야 한다.
  • 프로젝트를 시작할 때는 당연히 시스템의 일반적인 구조를 먼저 생각해야 한다. 하지만 변하는 환경에 대처해 진로를 변경할 능력도 반드시 유지해야 한다.
  • 도시든 소프트웨어 프로젝트든, 아주 큰 시스템에서는 한 사람이 모든 결정을 내리기 어렵다. 그리고 이것이 우리가 관심사를 분리해 각각의 책임을 가져야 하는 이유이다.
profile
make it mine, make it yours

0개의 댓글