1. 접근 제어자 : internal, open
Java는 클래스가 기본적으로 상속 가능한 상태 (open)이지만, Kotlin은 기본이 final이다.
이 설계는 "명시적으로 허용한 경우에만 상속하도록" 하여 설계 안정성을 높이기 위함이다.
- final (Java) : 변수 수정 및 methods/classes overriding 방지
- final (Kotlin) : overriding & 상속 방지
Kotlin의 주요 접근 제어자는 다음과 같다.
- public : 모듈 어디서나 접근 가능
- internal : 같은 모듈 내부에서만 접근 가능
- protected : 상속 관계에서만 접근 가능
- private : 해당 파일 또는 클래스 내부에서만 접근 가능
- open : 상속을 명시적으로 허용하는 키워드
또한 Kotlin은 all-open compiler plugin이 기본 내장되어 있어서, 특정 프레임워크(Spring)의 주석(annotation)이 달린 클래스는 자동으로 open 처리된다.
plugins {
kotlin("plugin.allopen") version "2.2.21"
}
→ 대표적으로 @Component, @Service, @Configuration 등.
실무에서 왜 중요한가?
- Spring 기반 프로젝트에서 상속이 필요한 proxying(AOP, CGLIB) 때문에 내부적으로 open이 필요하다.
- 모듈 경계를 명확히 하여 API 안정성을 증가시킨다.
- SDK나 공용 모듈을 만들 때 public vs. internal 전략이 굉장히 중요하다.
- Aspect Oriented Programming (AOP) : 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다.
- Code Generator Library (CGLIB) : 런타임에 동적으로 자바 클래스의 프록시를 생성한다. 인터페이스가 아닌 클래스에 대해서 동적 프록시를 생성할 수 있다.
코드 예제
// internal: 같은 모듈 내에서만 노출
internal class InternalUserService {
fun process() = "internal logic"
}
// open: 명시적으로 상속 허용
open class BaseController {
open fun handle() = "base handling"
}
class UserController : BaseController() {
override fun handle() = "user handling"
}
주의할 점
- 멀티 모듈 환경에서 internal이 모듈을 넘지 않는다는 것을 항상 인지해야 한다.
- Spring 환경에서는 대부분의 빈이 자동으로 open 처리되므로 상속 제약이 느슨해질 수 있다.
- 빈 (Bean) : 스프링 컨테이너에 의해 관리되는 재사용 가능한 소프트웨어 컴포넌트.
2. Kotlin keywords
inline : Lambda expression 성능 최적화
inline 함수는 호출 시 함수 호출 overhead를 제거하고, call site에 코드가 직접 삽입되는 방식으로 처리된다.
일반적으로 다음 목적에 많이 쓰인다.
- 고차 함수에서 Lambda의 객체 생성 비용 제거
- inline + reified 조합으로 제네릭 타입 유지
코드 예제
inline fun measure(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
println("took = ${System.currentTimeMillis() - start}ms")
}
fun main() {
measure {
Thread.sleep(500)
}
}
주의할 점
- inline이 많아지면 바이트코드 크기 증가 → 성능 저하
- 큰 함수는 inline 대상으로 적절하지 않다.
- Lambda 캡처 비용이 낮을 때만 유효하다.
reified : 제네릭 타입을 런타임까지 유지
Kotlin에서 제네릭은 기본적으로 타입 소거(Type Erasure)가 적용된다.
하지만 `inline fun + reified T` 조합을 사용하면 제네릭 타입을 런타임까지 유지할 수 있다.
실무에서 왜 중요한가?
- JSON 파싱 시 타입 안전한 변환
- Retrofit, Ktor API Response Wrapper에서 매우 자주 사용된다.
- Reflection 없이 타입 기반 처리가 가능하다.
코드 예제
inline fun <reified T> parseJson(json: String): T {
return ObjectMapper().readValue(json, T::class.java)
}
data class User(val id: Long, val name: String)
val user = parseJson<User>("""{ "id":1, "name":"G" }""")
주의할 점
- 반드시 inline함수에서만 사용 가능하다.
- 너무 많은 reified 사용은 프로젝트 복잡성을 증가시킨다.
3. 인터페이스 상속 (Spring 3.0 vs. 게임 개발)
- 게임 개발 : 객체 모델이 명확하고 도메인이 고정되어 있어 상속 계층 설계가 비교적 안정적이다.
- Spring 기반 서버 개발 : 동적 바인딩, AOP, Proxy 기반 구조로 인해 상속보다 구성(Compresison)을 강조한다.
- SOLID의 LSP는 "상속해도 계약이 유지되어야 한다"는 원칙이지, 상속을 써야한다는 의미가 아니다.
Spring 3.0 이후 인터페이스 상속은 실무에서 거의 쓰지 않는다.
대부분 전략 패턴, 빈 교체, DI 구조로 해결한다.
4. "com.github.johnrenglemen.shadwo" : fat jar의 META-INF 대체용 plug-in
- 기능
- 모든 의존성과 함께 패키징 (Fat jar)
- META-INF 충돌 문제 자동 처리
- `relocate` 기능을 통해 패키지 네임스페이스 충돌 해결
5. Server-Client 테스트 코드 설계 방식
Server 단위 테스트
- 비즈니스 로직이 요청 → 응답 구조에 맞게 동작하는지
- 예외 처리, 유효성 검증, 상태 변화 확인
- 소켓 / 네트워크 레이어는 mock 처리
Client 단위 테스트
- 사용자가 특정 action을 했을 때 → 올바른 패킷/요청이 생성되는지
- 서버의 응답을 올바르게 파싱·처리하는지
통신 자체는 단위 테스트에서 검증하지 않는 이유
- 소켓 / 네트워크는 I/O 기반이며 비결정적 요소(지연, 패킷 유실) 포함한다.
- 단위 테스트는 결정적(deterministic)이어야 한다.
- 서버-클라이언트 간 실제 통신 검증은 통합 테스트(E2E)의 역할.
코드 예제
@Test
fun `client should send correct login packet`() {
val socket = mock<Socket>()
val output = ByteArrayOutputStream()
whenever(socket.getOutputStream()).thenReturn(output)
val client = Client(socket)
client.login("g_user")
assertTrue(String(output.toByteArray()).contains("LOGIN:g_user"))
}
# Mock 서버 예시
class StubServerResponse : ServerApi {
override fun fetchData(): String = "OK"
}
주의할 점
- 단위 테스트에서 실제 네트워크 호출 금지.
- 통신 연동 테스트는 별도 테스트 스위트로 분리한다.
6. 테스트 커버리지 (Test Coverage)
참고 출처
- https://kotlinlang.org/docs/all-open-plugin.html#gradle
- https://engkimbs.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81AOP
- https://memodayoungee.tistory.com/151
- https://dev-wnstjd.tistory.com/440
-
Powered By. ChatGPT
'Backend' 카테고리의 다른 글
| [Backend] JVM - Primitive type과 Reference type (1) | 2025.12.02 |
|---|---|
| [Backend] ShadowJar 플러그인 적용 후 “기본 Manifest 속성이 없습니다” 오류 해결기 (0) | 2025.11.24 |
| [Backend] Jackson 기반 직렬화·역직렬화의 원리와 적용 절차 (0) | 2025.11.17 |
| [Backend] Slf4J 와 Logback 로깅 도입 가이드 (0) | 2025.11.17 |
| Mockito-inline 5.2.0 설정 시 문제 해결 가이드 (Kotlin + Gradle + JDK17) (0) | 2025.10.30 |