소개

1장 오브젝트와 의존관계

날짜
2022/02/26

Reflection, Default Constructor

Reflection을 이용해 오브젝트를 생성하기 때문에 Default Constructor가 필요하다고 이야기 함.
코드가 어떤식으로 작성되기 때문에 그런걸까?
val someClass = SomeClass::class.java val constructors = someClass.getDeclaredConstructor() constructors.isAccessible = true val instance = constructors.newInstance()
Kotlin
복사
Class에 대한 Reference 획득 → 해당 Class의 생성자 획득 → 생성자를 사용할 수 있게끔 접근 지정자 public 으로 변경 → 생성자를 이용해 인스턴스 생성
이와 같이 Default Constructor를 통한 Instance 생성 → 생성된 Instance에 Field 세팅(IoC 컨테이너를 통한 DI 등) 순서로 이루어지기 떄문에 Default Constructor가 필수적임.

관심사의 분리

애플리케이션을 설계할 때에는 기존의 의존관계가 변경되거나 어떠한 역할을 하는 오브젝트가 사라질 수 있음을 염두에 두고 설계해야만 한다.
하지만 쉽지는 않은 일이다. 그렇지만 조금이나마 쉽게 이를 실천할 수 있는 방법이 있다.
환경, 상황 그리고 요구사항이 변함에 따라서 기능 변경을 위한 코드 레벨의 변화를 최소한으로 줄일 수 있는 방향으로 항상 생각하라.
그럼 어떻게 변경이 일어날 때 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있을까? → 분리와 확장을 고려한 설계
분리와 확장 예시
1.
Database Source의 변경: MySQL → Oracle
2.
Log Format 변경: 20자리 텍스트 포맷 → 16자리 텍스트 포맷
3.
등등,,
만약 이러한 변경을 적용할 때 여러 곳에 break-change가 발생한다면? → ......
가장 쉽게 이를 해결하는 방법은 공통 로직의 분리 → DatabaseConnectionManager, Logger, ...

Template Method Pattern

Super-class 에 기본적인 흐름을 두고 기능 중 일부를 직접 구현하거나 Sub-class 에서 구현
abstract class UserDao { fun findById(id: Long): User { val connection = getConnection() // .... return User(/*....*/) } protected abstract fun getConnection(): Connection } class KakaoUserDao : UserDao() { override fun getConnection(): Connection { // ... return Connection(/*....*/) } } class NaverUserDao : UserDao() { override fun getConnection(): Connection { // ... return Connection(/*....*/) } }
Kotlin
복사

결국 객체지향 설계 원칙을 잘 지키는게 중요하다

단일 책임 원칙(SRP; Single Responsibility Principle) : 각 클래스는 고유한 하나의 역할만 지녀야 한다.
개방 폐쇄 원칙(OCP; Open-Closed Principle) : 확장엔 열려있고, 수정엔 닫혀있다. 즉 기존의 모듈을 수정하지 않고 확장할 수 있어야한다.
리스코프 치환 원칙(LSP; Liskov Substitution Principle) : 상위 타입의 객체를 하위 타입으로 치환해도 정상적으로 동작해야 한다
인터페이스 분리 원칙(ISP; Interface Segregation Principle) : Client에서 요구하는 Interfacing만 지원하게끔 인터페이스를 분리해야 한다.
의존관계 역전 원칙(DIP; Dependency Inversion Principle) : 저수준 모듈이 고수준 모듈에 의존해야 한다.
고수준 모듈: 알림
저수준 모듈: KakaoTalk 알림, SMS 알림, E-mail 알림, ...
// DipExam interface Alarm { fun beep(): String } class KakaoTalkAlarm : Alarm { override fun beep(): String { return "KakaoTalk" } } class SmsAlarm : Alarm { override fun beep(): String { return "Sms" } } class EmailAlarm : Alarm { override fun beep(): String { return "Email" } } class AlarmCenter(private val alarm: Alarm) { fun alarm() { println(alarm.beep()) } } fun main() { val kakaoTalkAlarm = KakaoTalkAlarm() val alarmCenter = AlarmCenter(kakaoTalkAlarm) alarmCenter.alarm() }
Kotlin
복사

Strategy Pattern

객체지향 설계 원칙에 따라 개발을 하다보면 가장 흔히 쓰이는 디자인 패턴 중 하나.
전략 패턴은 각 구현체별로 다르게 가져가야 하는 기능을 인터페이스를 통해 통째로 외부에 분리시키고, 이를 구현한 구현 클래스를 필요, 상황에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
그러다보니 객체지향 설계 원칙 중, 개방 폐쇄 원칙(OCP)의 실현에 가장 잘 들어맞는 패턴이라 볼 수 있다.

Object Factory Class

예제 소스코드

Spring Application Context의 동작방식

1.
별도로 정의된 설정정보를 기반으로 Bean Class의 시그니처 관리 ( @Configuration + @Bean / @Component ... )
2.
관리되고 있는 Bean Class Instance 생성 / 등록
3.
Client에서 Bean Class 조회 요청 시 생성해둔 Instnace 제공

Object Factory Class  Spring Application Context

1.
Application Context는 사용하는쪽에서 구체적인 팩토리 클래스를 알 필요 없게 만들어준다.
2.
Application Context는 Object Factory Class에서 제공하는 기능 + @를 제공한다. : Instance 자동 생성, 후처리, 필터, 인터셉터 등등,..
3.
결국 프레임워크 레벨에서 IoC를 비롯한 기능을 제공해주고 있기 때문에 개발 편의성이 크게 향상된다 볼 수 있다.

싱글톤 레지스트리(singleton registry)

Application Context 는 Object Factory Class와 비슷한 방식으로 동작하는 IoC 컨테이너 이면서 싱글톤 레지스트리이기도 하다. 스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다. Why?

Thread 관점에서의 Web Server Application

웹 (HTTP)은 클라이언트의 요청에 따라 새로운 연결이 수립되고, 요청의 종류에 따라 비즈니스 로직이 실행되며 관련 Instance가 생성되게 된다.
Instance 생성은 결국 메모리 할당이다. 만약 1억건의 요청이 들어왔다고 1억 x n개의 Instance를 생성한다면 어떻게 될까? → 메모리 boom

정리

확장, 지속 가능한 애플리케이션을 개발하기 위해선 객체와 각 객체간의 의존관계에 대해서 끝없이 고민해야한다.
결국 각 객체가 어떤 객체와 어떤 메시지를 주고 받아야하는지 정의하는건 개발자이다.
스프링은 원칙을 따라 개발하는데 있어 번거로운 작업을 줄여 이를 실천하는데 큰 도움을 준다.
overall dependency-injection-framework repository