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