티스토리 뷰

웹/Spring

스프링 볶음밥 - 7장

세댕댕이 2022. 7. 11. 16:32

 

 

 

 

[이전 글]

더보기

볶음밥 1장 - 1,2,3: 자바빈, 디자인 패턴(템플릿 메소드, 팩토리 메소드, 전략 패턴), 관심사의 분리, SOLID 및 객체지향 약간

볶음밥 1장 - 4,5,6: 제어의 역전, 프레임워크 vs 라이브러리, 스프링 IoC 및 용어 정리, 싱글톤, 동일성 vs 동등성, 빈의 스코프

볶음밥 1장 - 7: 의존관계 주입(DI), DL, IoC

볶음밥 2장: 테스트, TDD, jUnit

볶음밥 3장: 템플릿/콜백

볶음밥 4장: 예외

볶음밥 5장: 서비스 추상화, 테스트 대역

볶음밥 6장 - 1: AOP (1)

볶음밥 6장 - 2: AOP (2)

 

[7장] 스프링 핵심기술의 응용

스프링의 3대 핵심기술인 IoC/DI, 서비스 추상화, AOP는 객체지향적인 언어의 장점을 활용해 코드를 작성할 수 있도록 도와준다. 이를 개발에 활용해보자

 

[7.1] SQL과 DAO의 분리

DAO는 데이터를 가져오고 조작하는 작업의 인터페이스 역할을 수행한다.

- DB 테이블, 필드명 변경, 테이블 분리 등등.. SQL 문장이 변경된다면  SQL을 담고있는 DAO의 코드도 변경되게 된다.

- SQL 처럼 변경될 수 있는 텍스트로 된 정보는 외부 리소스에 담아두고 가져오도록 하는 것이 좋다.

 

-> JAXB(Java Architecture for XML Binding)을 이용해 XML에 담긴 정보를 파일에서 읽어오도록 해보자

 

근데.. 따라하기가 좀 골치아프다... 코드 작성은 어렵겠다 

 

* 빈 후처리기는 스프링 컨테이너가 빈을 생성한 뒤에 부가적인 작업을 수행할 수 있도록 해주는 기능이었다.

- 프록시 자동생성기

- 애노테이션을 이용한 빈 후처리기 -> @PostConstruct. 빈 생성 및 DI 작업(의존 오브젝트 및 설정값 세팅)이 모두 완료된 이후에 @PostConstruct가 붙은 메소드를 실행시켜준다. (init() 메소드 같은곳에 자주 사용됨)

 

 

* 인터페이스는 한 개 이상을 상속하는 것이 허용된다. 

- 인터페이스는 implements를 이용한 구현이기도 하지만, 일종의 상속의 개념으로도 생각할 수 있다. 인터페이스 구현은 타입을 상속하는 것으로 클래스를 상속하는 것과 마찬가지로 인터페이스를 구현하는 경우에도 구현 클래스는 인터페이스의 타입을 그대로 물려받는다. 덕분에 같은 타입으로 존재하지만 다른 구현을 가진, 다형성을 살린 개발이 가능하다.

 

@Setter
public class XmlSqlService implements SqlService, SqlRegistry, SqlReader {
    private SqlReader sqlReader;
    private SqlRegistry sqlRegistry;
    
    // SqlReader만 접근 
    private String sqlMapFile;
    
    // SqlRegistry만 접근
    private Map<String, String> sqlMap = new HashMap<>(); 

    @PostConstruct
    public void loadSql() {
        sqlReader.read(sqlRegistry);
    }

    @Override // SqlService
    public String getSql(String key) throws SqlRetrievalFailureException {
        try {
            return sqlRegistry.findSql(key);
        } catch (SqlNotFoundException e) {
            throw new SqlRetrievalFailureException(e);
        }
    }

    @Override // SqlRegistry
    public void registerSql(String key, String sql) {
        sqlMap.put(key, sql);
    }

    @Override // SqlRegistry
    public String findSql(String key) throws SqlNotFoundException {
        if(sqlMap.containsKey(key)) {
            return sqlMap.get(key);
        } else {
            throw new SqlNotFoundException(key + "에 대한 SQL을 찾을 수 없습니다");
        }
    }

    @Override // SqlReader
    public void read(SqlRegistry sqlRegistry) {
        System.out.println(sqlMapFile + "읽기");
        // 대충 외부 리소스에서 읽어오는 과정
        // sqlMapFile 어쩌고..
        // sqlRegistry.registerSql(key, val);
    }
}

위와 같이 하나의 구현 클래스가 여러개의 인터페이스를 다중 상속해도 된다.

- 이 경우 자기가 관리하는 인스턴스 변수가 아닌 경우에는 인터페이스를 통해서만 접근하도록 해야한다

 

* 디폴트 의존관계를 갖는 빈 설정

- 특정 의존 오브젝트가 대부분의 환경에서 거의 디폴트라고 해도 좋을만큼 기본적으로 사용될 가능성이 있다면, 디폴트 의존관계를 갖는 빈을 만드는 것을 고려해볼 수 있다.

public class DefaultSqlService extends BaseSqlService {
    public DefaultSqlService() {
        setSqlReader(new JaxbXmlSqlReader());
        setSqlRegistry(new HashMapSqlRegistry());
    }
}

//// 설정파일

<bean id="sqlService" class="springbook.sql.DefaultSqlService">
    <property name="sqlRegistry" ref="sqlRegistry"/> // 바꿔 끼울것이 있다면 바꿀 수 있다
</bean>

DI 설정이 따로 없을 경우에는 디폴트로 적용하고 싶은 의존 오브젝트를 생성자를 통해 미리 직접 넣어두는 방식.

-> 특별한 DI가 필요하지 않은 경우에는 기본적으로 설정되어있는 오브젝트를 그대로 사용할 수 있다.

 

* 생성자를 통해서 직접 디폴트 의존관계를 넣을때는 프로퍼티를 외부에서 직접 넣어줄 수 없으니 간접 DI로 대신 넣어주거나, 의존 오브젝트에 static final으로 디폴트 값을 넣어버려서 사용하면 유용하다.

 

=> DI를 사용한다고 항상 모든 프로퍼티 값을 설정에 넣어두고 모든 의존 오브젝트를 빈으로 지정해야 하는 것이 아니다. 디폴트 의존 오브젝트를 사용해서 특별한 설정 없이도 사용가능 하도록 만들어둘 수 있다!

= 준비된 기능을 손쉽게 사용 가능하면서도 확장에는 열려있는 구조!

 

* 단점 = 설정을 통해 다른 구현 오브젝트를 사용한다고 해도 생성자에서 일단 디폴트 의존 오브젝트를 다 만들어버린다.

-> 쓰지도 않는 오브젝트가 만들어지게 된다는 것. 

-> @PostConstruct 초기화 메소드를 이용해 프로퍼티가 설정되었는지 확인한 이후에 디폴트 오브젝트를 생성하는 식으로 대처 가능.

 

 

[DI를 의식하는 설계]

DI를 적용하려면 커다란 오브젝트 하나만 존재해서는 안된다. 최소한 두 개 이상, 의존관계를 가지고 협력하는 오브젝트가 있어야 한다. 따라서 적절한 책임에 따라 오브젝트를 분리해야 한다.

+ DI는 런타임 시에 의존 오브젝트를 다이내믹하게 연결해 유연한 확장을 꾀하는 것이 목적이므로 항상 확장에 염두를 두고 오브젝트 간의 관계를 생각해야 한다.     "DI는 결국 미래를 프로그래밍 하는 것이다"

 

DI를 적용할 때에는 가능한 한 인터페이스를 사용하도록 해야한다.

- 물론 인터페이스가 아니더라도 DI는 가능하다. 의존 오브젝트가 생성자나 수정자를 통해 주입만 가능하면 되기에 의존 오브젝트의 클래스 타입을 클라이언트가 직접 사용해도 문제는 없다. 다만, DI다운 개발을 위해서는 두 개의 오브젝트가 인터페이스를 통해 느슨하게 연결되어야 한다.

 

인터페이스를 사용해야 하는 이유

1. 다형성을 위해.

- 하나의 인터페이스를 통해 여러 개의 구현을 바꿔가며 사용할 수 있도록 하는 것이 DI가 추구하는 목적이다. 

 

2. 인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확히 할 수 있다.

- A 오브젝트가 B 오브젝트를 사용한다고 했을 때 (=> A: 클라이언트 / B: 의존 오브젝트) A와 B가 인터페이스로 연결되어 있다는 것은 A가 B를 바라볼 때 해당 인터페이스라는 창을 통해서만 본다는 의미이다.

- 만약 B가 B1이라는 인터페이스를 구현하고 있다면, 그리고 A는 B1 인터페이스를 통해서만 B를 사용한다면 A에게 B는 B1이라는 관심사를 구현한 임의의 오브젝트에 불과하다. 따라서 같은 인터페이스를 구현한 C, D 클래스가 대신 들어가더라도 A에게 DI가 가능해진다.

 

그런데 B가 B1 인터페이스 뿐만 아니라 B2 인터페이스도 구현하고 있을 수도 있다. 인터페이스는 하나의 오브젝트가 여러 개를 구현할 수 있다. 하나의 오브젝트를 바라보는 창이 여러개일 수 있다는 것.

-> 각기 다른 관심과 목적으로 어떤 오브젝트에 의존하고 있다는 것

-> 인터페이스를 클라이언트의 종류에 따라 적절하게 분리해서 오브젝트가 구현하도록 하자 (B는 B1, B2 인터페이스를 다중 구현했지만 A는 B1이라는 창을 통해서만 B를 그대로 사용하면 된다) 

=> 목적과 관심이 각기 다른 클라이언트가 있다면, 인터페이스를 통해 적절하게 분리해 주는 것이 좋다. 이를 바로 인터페이스 분리 원칙, ISP라고 부른다. = 클라이언트에 특화된 의존관계를 만들어낼 수 있다

 

그냥 복잡한 것 필요 없이 DI는 특별한 이유가 없는 한 인터페이스를 사용해야 한다고 생각하자.

DI에서 굳이 인터페이스를 써야 하냐고 묻는데 논리적으로 반박할 자신이 없다면 그냥 DI는 원래 인터페이스를 사용해야 한다고 우겨도 좋다 :P

 

하나의 오브젝트가 구현하는 인터페이스를 여러 개 만들어서 구분하는 이유 중 하나는, 오브젝트 기능이 발전하는 과정 속에서 다른 종류의 클라이언트가 등장할 수 있기 때문이다. 이때 때로는 인터페이스를 여러 개 만드는 대신 인터페이스 상속을 통한 확장을 하는 것이 좋을 수도 있다.

- ISP가 주는 장점은, 모든 클라이언트가 자신의 관심에 따른 접근 방식을 불필요한 간섭 없이 유지할 수 있다는 것이다. 덕분에 기존 클라이언트에는 영향을 주지 않은 채로도 오브젝트의 기능을 확장하거나 수정할 수 있다. (기존 클라이언트는 자신이 사용하던 인터페이스를 통해 동일한 방식으로 접근할 수만 있다면 오브젝트의 변경에는 아무런 영향이 없다)

 

p.621

public interface SqlRegistry {
    void registerSql(String key, String sql);
    String findSql(String key) throws SqlNotFoundException;
}

//////

public interface UpdatableSqlRegistry extends SqlRegistry {
    public void updateSql(String key, String sql) throws SqlUpdateFailureException;
    public void updateSql(Map<String, String> sqlMap) throws SqlUpdateFailureException;
}

p623.

@Setter
public class SqlAdminService {
    private UpdatableSqlRegistry updatableSqlRegistry;

    public void updateMethod() {
        updatableSqlRegistry.updateSql("key", "sql");
        ...
    }

    public void findMethod() {
        updatableSqlRegistry.findSql("key");
        ...
    }

    public void registerMethod() {
        updatableSqlRegistry.registerSql("key", "sql");
        ...
    }
}

BaseSqlService와 SqlAdminService는 모두 동일한 MyUpdateSqlRegistry라는 오브젝트를 DI 받아서 사용하지만, 각자의 관심과 필요에 따라서 서로 다른 인터페이스를 통해 접근한다

 

* 이떄 SqlAdminService는 SqlRegistry 인터페이스 메소드랑 UpdatableSqlRegistry 인터페이스 메소드 모두 사용가능.

- SQL 수정 기능만을 필요로하는 클라이언트가 필요했다면 아예 상속 대신 새로운 인터페이스를 추가했을 수도 있다. 이는 오브젝트 사이의 의존관계와 목적에 따라 적절히 선택하면 된다.

- 중요한 것은 클라이언트가 정말 필요한 기능을 가진 인터페이스를 통해 오브젝트에 접근하도록 만들었는가? 이다.

= 잘 적용된 DI는 잘 설계된 오브젝트 의존관계에 달려있다.

 

 

[스프링의 변화]

1. 애노테이션의 메타정보 활용

자바는 소스코드가 컴파일 된 이후 클래스 파일에 저장되었다가 JVM에 의해 메모리로 로딩되어 실행된다. 그런데 때때로 자바 코드가 실행되는 것이 목적이 아니라 다른 자바 코드에 의해 데이터처럼 취급되기도 한다. 자바 코드의 일부를 리플렉션 API를 이용해 어떻게 만들었는지 살펴보고, 그에 따라 동작하도록 하는 기능이 많아지고 있다.

-> 대표적인 방법이 바로 애노테이션을 활용하는 것

 

자바 클래스/인터페이스, 필드, 메소드 등은 그 자체로 실행 가능하고 상속 및 참조가 가능하다. 반면 애노테이션은 기존의 자바 프로그래밍 방식으로는 사용할 수 없다. 애노테이션은 옵션에 따라 컴파일된 클래스에 존재하거나 애플리케이션이 동작할 때 메모리에 로딩되기도 하지만, 자바 코드가 실행되는데 직접 참여하지는 못한다. 애노테이션 자체가 클래스의 타입에 영향을 주지도 못하고, 일반 코드에서 활용되지도 못하기 때문에 디자인 패턴에도 적용할 수 없다.

하지만 리플렉션 API를 통해 애노테이션의 메타정보를 조회하고, 애노테이션 내에 설정된 값을 가져와 참고하도록 할 수 있다.

 

애노테이션의 활용이 증가한 이유는 핵심 로직을 담은 자바 코드와 이를 지원하는 IoC 방식의 프레임워크, 그리고 프레임워크가 참조하는 메타정보 3가지로 구성되는 방식에 잘 어울리기 때문이다.

-> 여기서 메타 정보로 사용하기에 가장 잘 어울리는 것이 바로 애노테이션.

 

이전까지는 XML을 통해서 오브젝트간의 관계를 설정하는 메타정보를 구성해 왔었다. 

XML과 비교했을 때 애노테이션을 통한 메타정보 구성이 갖는 이점이 훨씬 많다.

장점) 1. 애노테이션 하나만 붙여주는 것만으로도 매우 많은 부가정보를 제공해줄 수 있다.

2. XML으로 메타정보를 작성하는 것은 오타 가능성도 많고 리팩토링도 까다롭다 

 

단점) XML은 어느 환경에서나 손쉽게 편집이 가능하고, 내용이 변경되더라도 다시 빌드를 할 필요가 없다. 하지만 애노테이션은 자바 코드에 존재하므로 변경될 때 마다 빌드를 거쳐야 한다.

 

이러한 장점들로 인해 스프링 3.1부터는 XML 없이 애노테이션만으로도 거의 모든 메타정보 작성이 가능하도록 개선된다

 

 

2. 정책과 관례를 이용한 프로그래밍

애노테이션과 같은 메타정보를 활용하는 프로그래밍 방식은, 코드를 이용해 명시적으로 동작 내용을 기술하는 대신 미리 악속한 규칙 또는 관례를 따라서 프로그램이 동작하도록 만드는 스타일을 적극적으로 포용하도록 만들었다,

장점: 자주 반복되는 부분을 관례화 하고, 규칙을 정의해둠으로써 코드가 간결해진다.

단점: 언어 및 API 사용법 이외에도 미리 정의된 관례와 규칙들을 학습해야 한다

 

 

* @Configuration이 붙은 자바 클래스를 DI 메타정보로 이용하면, XML에 있는 <context:annotation-config />는 필요 없다

(XML에 담긴 DI 정보를 이용하는 경우에는 위 태그를 입력해서 빈 후처리기를 따로 등록시켜 줘야 했다)

 

* @Autowired를 이용한 자동 와이어링

- 자동 와이어링 기능을 이용해 조건에 맞는 빈을 찾고, 생성자나 수정자 메소드, 필드에 자동으로 넣어준다.

- 컨테이너가 이름이나 타입을 기준으로 주입될 빈을 찾아주기 때문에 빈 프로퍼티 설정 코드가 대폭 줄어든다. 컨테이너가 자동으로 주입할 빈을 결정하기 어려운 경우에만 직접 프로퍼티에 주입할 대상을 지정해 주는 방식으로 병행하면 된다.

- 장점: DI 관련 설정 코드를 대폭 줄일 수 있다

- 단점: 빈 설정정보를 보고 다른 빈과 의존관계가 어떻게 맺어졌는지를 한눈에 보기 어렵다

 

** 필드에 @Autowired를 바로 때려박는 것은 권장하지 않는다

- 스프링과 무관하게 직접 오브젝트를 생성하고 다른 오브젝트를 주입해서 테스트하는 순수한 단위 테스트를 만드는 경우에는 생성자/수정자 메소드가 필요하다. 

- 그냥 생성자 주입을 합시다! (final 설정 가능 / 순환 참조 방지 등... 안 쓸 이유가 없다.)

 

* @Component를 이용한 자동 빈 등록

- @Component는 클래스에 부여되고, @Component가 붙은 클래스는 빈 스캐너를 통해 자동으로 빈으로 등록된다.

- @Configuration 설정 파일에서는 @ComponentScan 애노테이션을 이용해 basePackage를 기준으로 @Component가 붙은 클래스를 탐색한다. (빈의 아이디는 다른 설정이 없다면 클래스의 이름 첫 글자를 소문자로 바꾼 뒤 사용)

-> @Component를 이용한 자동 빈 등록을 이용하는 경우 빈의 의존관계를 담은 프로퍼티를 따로 지정할 방법이 없다. 그래서 프로퍼티 설정에 @Autowired 자동 와이어링 방식을 적용해야 한다. 이 둘은 땔래야 땔 수 없는 찰떡관계.

 

서비스 계층은 @Service

DAO 기능을 제공하는 클래스에는 @Repository 애노테이션을 붙이자 (메타 애노테이션, @Component를 포함하고 있음)

 

 

나머지 부분은 빠르게 생략...

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함