반응형
예제에 대한 전체 코드는 https://github.com/minseokLim/practice/tree/main/design-pattern에서 확인할 수 있다.

 

파사드 패턴(Facade pattern)은 여러 개의 클래스나 서브시스템을 감싸는 단일 인터페이스를 제공하여, 클라이언트가 복잡한 서브시스템과 상호작용 하는 것을 쉽게 만들어주는 패턴이다.

 

예제를 통해 좀 더 자세히 이 패턴에 대해 알아보자. 예제는 코틀린으로 작성하였다.

 

만약 한 사원에 대한 보고서를 생성해야하는 상황이 생겼다고 가정하자. 이 보고서는 사원 정보(Emp), 이력서 정보(Resume), 동료 평가 정보(Evaluation)를 바탕으로 생성된다. 이 3가지 정보는 각각의 DB 테이블에 저장되어있고, 각각 DAO 클래스를 제공한다. 아래 코드는 각각의 DAO를 감싸는 파사드 클래스의 예시이다.

class EmpReportDaoFacade(
    private val empDao: EmpDao,
    private val resumeDao: ResumeDao,
    private val evaluationDao: EvaluationDao
) : EmpReportFacade {
    override fun select(id: Int): EmpReport {
        val emp = empDao.select(id) ?: throw IllegalArgumentException("No employee found for id $id")
        val resume = resumeDao.select(id) ?: throw IllegalArgumentException("No resume found for id $id")
        val evaluation = evaluationDao.select(id) ?: throw IllegalArgumentException("No evaluation found for id $id")

        return EmpReport(
            employeeId = emp.id,
            employeeName = emp.name,
            phone = resume.phone,
            address = resume.address,
            evaluationGrade = evaluation.grade,
            evaluationComment = evaluation.comment
        )
    }
}

이렇게 하면 사용자는 각각의 DAO에 접근하는 대신, 파사드를 통해서 원하는 데이터를 얻을 수 있다. 보고서 정보를 원하는 곳이 여러 곳일 경우 코드 중복을 방지할 수 있다.

 

위 코드에서는 EmpReportDaoFacade가 EmpReportFacade 인터페이스를 구현하고 있다. 인터페이스를 따로 정의하면, 클라이언트의 변경 없이 서브시스템 자체를 변경할 수 있다는 장점이 있다. 위 예제에서 만약 관련 정보를 DB 테이블에서 직접 조회하는 것 대신, HTTP 요청을 통해 조회하는 것으로 변경되었다고 가정하자. 아래와 같이 EmpReportFacade 인터페이스를 구현하는 또 다른 파사드 클래스를 만들어주면, 클라이언트 코드의 변경 없이 대응이 가능하다.

class EmpReportHttpFacade(
    private val empHttpClient: EmpHttpClient,
    private val resumeHttpClient: ResumeHttpClient,
    private val evaluationHttpClient: EvaluationHttpClient
) : EmpReportFacade {
    override fun select(id: Int): EmpReport {
        val emp = empHttpClient.get(id) ?: throw IllegalArgumentException("No employee found for id $id")
        val resume = resumeHttpClient.get(id) ?: throw IllegalArgumentException("No resume found for id $id")
        val evaluation = evaluationHttpClient.get(id) ?: throw IllegalArgumentException("No evaluation found for id $id")

        return EmpReport(
            employeeId = emp.id,
            employeeName = emp.name,
            phone = resume.phone,
            address = resume.address,
            evaluationGrade = evaluation.grade,
            evaluationComment = evaluation.comment
        )
    }
}

 

 

이 글은 "객체 지향과 디자인 패턴 - 최범균" 책을 기반으로 작성되었다.

반응형
반응형
예제에 대한 전체 코드는 https://github.com/minseokLim/practice/tree/main/design-pattern에서 확인할 수 있다.

 

데코레이터 패턴(Decorator Pattern)은 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴이다. 데코레이터 패턴은 컴포지션(composition)과 위임(delegation)을 사용하여 원래 객체의 인터페이스를 유지하면서 기능을 추가하는 방법이다.

 

주요 구성 요소는 다음과 같다.

  • 컴포넌트(Component) : 기본 인터페이스나 추상 클래스로, 구체적인 기능을 정의한다. 데코레이터와 구체적인 컴포넌트 객체 모두 이 컴포넌트를 구현한다.
  • 구체적인 컴포넌트(Concrete Component) : 컴포넌트 인터페이스를 구현한 클래스이다. 컴포넌트 인터페이스에서 정의된 기능을 구현한다.
  • 데코레이터(Decorator) : 컴포넌트 인터페이스를 구현하거나 확장한 추상 클래스이다. 이 클래스는 컴포넌트 객체를 포함하며, 컴포넌트의 메서드를 호출(위임)하는 메서드를 갖는다.
  • 구체적인 데코레이터(Concrete Decorator) : 데코레이터를 확장한 클래스이다. 컴포넌트 인터페이스에서 정의된 기능을 구현함으로써 동적으로 추가될 기능을 구현한다.

예제를 통해 좀 더 자세히 이 패턴에 대해 알아보자. 예제는 코틀린으로 작성하였다.

 

아래는 예제 소스 코드의 클래스 다이어그램이다.

FileOut은 컴포넌트 인터페이스에 해당한다. 문자열(String)의 내용을 파일에 쓰는 기능을 정의한다. FileOutImpl은 구체적인 컴포넌트에 해당한다. 실제로 파일에 쓰는 기능을 구현한다.

 

아래는 Decorator 클래스의 코드이다.

abstract class Decorator(
    private val delegate: FileOut
) : FileOut {
    fun doDelegate(data: String) {
        delegate.write(data)
    }
}

데코레이터 클래스는 컴포넌트 인터페이스에 해당하는 FileOut을 구현함과 동시에, FileOut 객체를 주입받는다. 그리고 주입받은 객체에 기능 실행을 위임하는 doDelegate() 메서드를 가진다.

 

다음은 데코레이터 클래스를 확장한 구체적인 데코레이터 클래스의 예시이다.

class EncryptionOut(
    delegate: FileOut
) : Decorator(delegate) {
    override fun write(data: String) {
        val encryptedData = encrypt(data)
        super.doDelegate(encryptedData)
    }

    private fun encrypt(data: String): String {
        println("data is encrypted.")
        return "encrypted $data"
    }
}

구체 클래스인 EncryptionOut은 파일의 내용을 암호화하는 기능을 가졌다고 가정해보자. 위 코드를 보면, input으로 들어온 data를 암호화하고 그 결과값을 데코레이터의 doDelegate메서드에 전달함으로써, 주입받은 FileOut 객체에 기능 실행을 위임한다.

 

이제 클라이언트 코드에서 데코레이터 패턴을 사용함으로써, 동적으로 객체의 기능을 추가할 수 있다. 아래 예시를 살펴보자.

class DecoratorTest {

    @Test
    fun write() {
        // given
        val fileOut = FileOutImpl() // 일반적인 파일 쓰기 기능
        val encryptionOut = EncryptionOut(fileOut) // EncryptionOut 데코레이터를 적용함으로써 암호화 기능을 추가
        
        // when, then
        assertDoesNotThrow { encryptionOut.write("data") }

        // given
        val zipEncryptionOut = ZipOut(EncryptionOut(fileOut)) // ZipOut 데코레이터를 적용함으로써 압축 기능을 추가

        // when, then
        assertDoesNotThrow { zipEncryptionOut.write("data") }

        // given
        val encryptionZipOut = EncryptionOut(ZipOut(fileOut)) // EncryptionOut, ZipOut을 순차적으로 적용

        // when, then
        assertDoesNotThrow { encryptionZipOut.write("data") }
    }
}

 

우리가 흔히 접할 수 있는 코드 중 데코레이터 패턴이 적용된 대표적인 예는 스프링 프레임워크의 트랜잭션 처리이다. @Transactional 어노테이션을 응용 서비스 클래스에 붙이면, 트랜잭션 기능이 추가된 데코레이터 객체(프록시 객체)를 런타임에 생성하여 주입한다.

 

 

이 글은 "객체 지향과 디자인 패턴 - 최범균" 책을 기반으로 작성되었다.

반응형
반응형
예제에 대한 전체 코드는 https://github.com/minseokLim/practice/tree/main/design-pattern에서 확인할 수 있다.

 

컴포지트 패턴(Composite Pattern)은 클라이언트가 개별 객체와 객체 그룹을 동일하게 처리할 수 있도록 해주는 패턴이다. 주로 복합 객체와 단일 객체를 동일한 인터페이스로 다뤄야 할 때 유용하다.

 

주요 구성 요소는 다음과 같다.

  • 컴포넌트(Component) : 공통 인터페이스 또는 추상 클래스를 정의하여 리프(Leaf)와 복합체(Composite)에서 공통으로 사용할 메서드를 선언한다. 이 인터페이스를 통해 클라이언트는 복합 객체와 개별 객체를 동일하게 다룰 수 있다.
  • 리프(Leaf) : 컴포넌트의 기본 단위로, 더 이상 하위 요소를 포함하지 않는 객체를 나타낸다. 리프는 컴포넌트 인터페이스를 구현하며, 일반적으로 실제 작업을 수행하는 역할을 한다.
  • 복합체(Composite) : 컴포넌트를 자식으로 포함할 수 있는 복합 객체를 나타낸다. 복합체는 컴포넌트 인터페이스를 구현하고, 자식 컴포넌트를 추가, 제거 및 접근할 수 있는 메서드를 제공한다. 또한, 자식 객체의 작업을 수행하는 역할을 한다.

 

예제를 통해 좀 더 자세히 이 패턴에 대해 알아보자. 예제는 코틀린으로 작성하였다.

아래는 예제 소스 코드의 클래스 다이어그램이다.

Device 인터페이스는 컴포넌트에 해당한다. 리프에서 기본적인 작업을 수행할 turnOn()/turnOff() 메서드와 복합체에서 자식 컴포넌트를 추가할 addDevice()/removeDevice() 메서드를 선언한다.

Light, Aircon은 리프에 해당하고 DeviceGroup은 복합체에 해당한다.

 

아래는 Light와 Aircon의 상세 구현이다. 리프 객체에 자식 컴포넌트를 추가하는 것은 불가능하므로  addDevice()/removeDevice() 메서드에서 각각 UnsupportedOperationException이 발생한다. canContain() 메서드는 자식 컴포넌트를 가질 수 있는지의 여부를 체크하는 메서드로, 여기에선 false를 리턴한다.

class Light : Device {
    override fun addDevice(device: Device) {
        throw UnsupportedOperationException("Light cannot contain any device")
    }

    override fun removeDevice(device: Device) {
        throw UnsupportedOperationException("Light cannot contain any device")
    }

    override fun canContain(device: Device): Boolean {
        return false
    }

    override fun turnOn() {
        println("Light is on")
    }

    override fun turnOff() {
        println("Light is off")
    }
}
class Aircon : Device {
    override fun addDevice(device: Device) {
        throw UnsupportedOperationException("Aircon cannot contain any device")
    }

    override fun removeDevice(device: Device) {
        throw UnsupportedOperationException("Aircon cannot contain any device")
    }

    override fun canContain(device: Device): Boolean {
        return false
    }

    override fun turnOn() {
        println("Aircon is on")
    }

    override fun turnOff() {
        println("Aircon is off")
    }
}

 

다음은 DeviceGroup의 상세 구현이다. 여기서 주목해야할 것은, turnOn()/turnOff() 메서드 호출 시, 자식 컴포넌트들의 작업 메서드들을 호출한다는 것이다.

class DeviceGroup : Device {
    private val devices = mutableListOf<Device>()

    override fun addDevice(device: Device) {
        devices.add(device)
    }

    override fun removeDevice(device: Device) {
        devices.remove(device)
    }

    override fun canContain(device: Device): Boolean {
        return true
    }

    override fun turnOn() {
        devices.forEach { it.turnOn() }
    }

    override fun turnOff() {
        devices.forEach { it.turnOff() }
    }
}

이렇게 되면 클라이언트에서는 리프(Leaf)와 복합체(Composite)를 구분 없이 동일한 인터페이스로 다루는 것이 가능해진다.

 

아래는 이를 사용한 클라이언트 코드 예시이다.

class PowerController(
    private val device: Device
) {
    fun turnOn() {
        device.turnOn()
    }

    fun turnOff() {
        device.turnOff()
    }
}
class PowerControllerTest {

    @Test
    fun turnOn() {
        // given - 리프 객체를 PowerController에 주입
        val light: Device = Light()
        val powerController1 = PowerController(light)

        // when, then
        assertDoesNotThrow { powerController1.turnOn() }

        // given - 복합체 객체를 PowerController에 주입
        val aircon: Device = Aircon()
        val deviceGroup: Device = DeviceGroup()
        if (deviceGroup.canContain(light)) {
            deviceGroup.addDevice(light)
        }
        if (deviceGroup.canContain(aircon)) {
            deviceGroup.addDevice(aircon)
        }
        val powerController2 = PowerController(deviceGroup)

        // when, then
        assertDoesNotThrow { powerController2.turnOn() }

		// PowerController 입장에서는 주입되는 객체가 리프인지 복합체인지 알 필요가 없음
        
        // given - DeviceGroup 안에 DeviceGroup를 추가하는 것도 가능 
        val lightGroup: Device = DeviceGroup()
        if (lightGroup.canContain(light)) {
            lightGroup.addDevice(light)
        }
        val airconGroup: Device = DeviceGroup()
        if (airconGroup.canContain(aircon)) {
            airconGroup.addDevice(aircon)
        }
        val totalGroup: Device = DeviceGroup()
        if (totalGroup.canContain(lightGroup)) {
            totalGroup.addDevice(lightGroup)
        }
        if (totalGroup.canContain(airconGroup)) {
            totalGroup.addDevice(airconGroup)
        }
        val powerController3 = PowerController(totalGroup)

        // when, then
        assertDoesNotThrow { powerController3.turnOn() }
    }
}

장점

  • 트리 구조로 복잡한 객체를 관리 : 복잡한 객체를 트리 구조로 구성하여 각 객체를 일관된 방식으로 관리할 수 있다.
  • 단순화된 클라이언트 코드 : 클라이언트는 개별 객체와 복합 객체를 구분하지 않고 동일한 방식으로 처리할 수 있어 코드가 단순해진다.
  • 유연성 : 새로운 종류의 컴포넌트를 추가해도 기존 코드를 수정할 필요가 없다.

단점

  • 모든 메서드의 의미가 명확하지 않을 수 있음: 복합체와 리프가 공통 인터페이스를 구현해야 하기 때문에, 일부 메서드가 리프에서는 의미가 없을 수 있다.
  • 디버깅과 유지보수의 어려움: 트리 구조가 깊어질수록 디버깅과 유지보수가 어려워질 수 있다.

 

 

이 글은 "객체 지향과 디자인 패턴 - 최범균" 책을 기반으로 작성되었다.

반응형
반응형
예제에 대한 전체 코드는 https://github.com/minseokLim/practice/tree/main/design-pattern에서 확인할 수 있다.

 

어댑터 패턴(Adapter Pattern)은 클라이언트가 요구하는 인터페이스와 사용하려는 모듈의 인터페이스가 일치하지 않을 때 사용할 수 있는 디자인 패턴이다. 어댑터 패턴의 주요 구성요소는 다음과 같다.

  • 타겟 인터페이스(Target Interface): 클라이언트가 사용할 인터페이스이다.
  • 어댑터(Adapter): 타겟 인터페이스를 구현하고, 어댑티(Adaptee)의 인터페이스를 타겟 인터페이스에 맞게 변환한다.
  • 어댑티(Adaptee): 사용하려는 모듈이다.

예제를 통해 좀 더 자세히 이 패턴에 대해 알아보자. 예제는 코틀린으로 작성하였다.

아래의 코드에서 SearchService는 타겟 인터페이스이고, WebSearchRequestHandler는 이 인터페이스를 사용하는 클라이언트 코드이다.

interface SearchService {
    fun search(keyword: String): SearchResult
}
class WebSearchRequestHandler(
    private val searchService: SearchService
) {
    fun handleSearchRequest(keyword: String): SearchResult {
        return searchService.search(keyword)
    }
}

 

원래는 DB에서 검색 결과를 반환하는 서비스를 제공했다고 생각해보자. 아래는 이를 구현한 DBSearchService 예시이다.

class DBSearchService(
    private val db: Set<String>
) : SearchService {
    override fun search(keyword: String): SearchResult {
        return SearchResult(db.filter { it.contains(keyword) })
    }
}

 

그런데 DB 데이터가 많아지면서 like 검색을 통한 DB 검색의 성능이 떨어지는 현상이 생겼다고 생각해보자. 그래서 별도의 검색 엔진 도입이 필요하다는 요구 사항이 주어졌다. 새로 도입할 검색 엔진은 TolrClient로 아래와 같은 인터페이스를 제공한다. 이 인터페이스는 기존의 인터페이스와는 형태가 맞지 않는데, 이 때 도입할 수 있는 패턴이 바로 어댑터 패턴이다.

class TolrClient(
    private val tolr: Set<String>
) {
    fun query(query: TolrQuery): TolrResponse {
        return TolrResponse(tolr.filter { it.contains(query.keyword) }.toSet())
    }
}

 

아래 코드는 어댑터 패턴을 적용한 어댑터 클래스 예시이다. 

class SearchServiceTolrAdapter(
    tolr: Set<String>
) : SearchService {
    private val tolrClient = TolrClient(tolr)

    override fun search(keyword: String): SearchResult {
        val query = TolrQuery(keyword)
        val response = tolrClient.query(query)
        return SearchResult(response.contents.toList())
    }
}

 

어댑터 클래스는 기존의 SearchService 인터페이스를 구현하면서 TolrClient 사용을 가능하게 해준다. 이로써 클라이언트 코드는 수정될 필요가 없이 변경된 어댑터 클래스를 주입해주면 된다.

class WebSearchRequestHandlerTest {

    @Test
    fun handleSearchRequest() {
        // given
        val db = setOf("apple", "banana", "cherry")
        val webSearchRequestHandler1 = WebSearchRequestHandler(DBSearchService(db))

        // when
        val result1 = webSearchRequestHandler1.handleSearchRequest("a")

        // then
        assert(result1.contents.contains("apple"))
        assert(result1.contents.contains("banana"))
        assert(!result1.contents.contains("cherry"))

        // given
        val tolr = setOf("apple", "banana", "cherry")
        val webSearchRequestHandler2 = WebSearchRequestHandler(SearchServiceTolrAdapter(tolr))

        // when
        val result2 = webSearchRequestHandler2.handleSearchRequest("a")

        // then
        assert(result2.contents.contains("apple"))
        assert(result2.contents.contains("banana"))
        assert(!result2.contents.contains("cherry"))
    }
}

 

어댑터 패턴이 적용된 예로는 Slf4j라는 로깅 API가 있다. Slf4j는 단일 로깅 API를 사용하면서 자바 로깅, log4j, logback 등의 로깅 프레임워크를 선택적으로 사용할 수 있도록 해주는데, 이 때 Slf4j가 제공하는 인터페이스와 각 로깅 프레임워크를 맞춰 주기 위한 어댑터를 사용하고 있다.

어댑터 패턴은 개방 폐쇄 원칙을 따를 수 있도록 도와준다. Slf4j를 예로 들면, Slf4j에서 제공하는 인터페이스를 통해 로깅 프레임워크를 사용해왔다면, 로깅 프레임워크를 log4j -> logback으로 클라이언트 코드 수정 없이 변경할 수 있다.

 

 

이 글은 "객체 지향과 디자인 패턴 - 최범균" 책을 기반으로 작성되었다.

반응형
반응형
예제에 대한 전체 코드는 https://github.com/minseokLim/practice/tree/main/design-pattern에서 확인할 수 있다.

 

추상 팩토리 패턴(Abstract Factory Pattern)은 객체 생성과 관련된 디자인 패턴 중 하나로서, 구체적인 클래스에 의존하지 않고, 인터페이스를 통해 연관된 객체들을 생성할 수 있게 해준다. 이렇게 하면 코드의 유연성과 확장성을 높일 수 있다.

 

추상 팩토리 패턴을 구성하는 요소에는 다음과 같은 것들이 있다.

  • 추상 팩토리(Abstract Factory) : 객체 생성을 담당하는 인터페이스를 선언한다. 구체적인 팩토리 클래스들이 이 인터페이스를 구현한다.
  • 구체적인 팩토리(Concrete Factory) : 추상 팩토리 인터페이스를 구현하여 실제 객체를 생성한다. 각 구체적인 팩토리는 특정한 제품 객체들을 생성하도록 메서드를 재정의한다.
  • 추상 제품(Abstract Product) : 생성될 객체들의 타입을 정의하는 인터페이스나 추상 클래스이다.
  • 구체적인 제품(Concrete Product) : 추상 제품 인터페이스를 구현한 클래스들로, 실제로 생성되는 객체들이다.

예제를 통해 좀 더 자세히 이 패턴에 대해 알아보자.

 

아래는 예제 소스 코드의 클래스 다이어그램이다.

 

비행기로 적들을 격추하는 게임을 생각해보자. 작은 비행기들이 초반에 나오다가 스테이지 마지막에 보스가 나오는 식으로 진행이 될 것이다. 미사일만 쏘는 작은 비행기들도 있을 거고, 무작정 돌진하는 비행기가 있을 수도 있다. 보스 중에는 강한 공격을 하는 보스가 있을 수도 있고, 자가 복제를 하는 보스가 있을 수도 있다. 쉬운 스테이지에는 StrongAttackBoss, DashSmallFlight 가 나오고 어려운 스테이지에는 CloningBoss, MissileSmallFlight 가 나와야 한다고 생각해보자.

 

단순하게 생각하면 이 코드를 작성할 때 if-else 문을 통해 스테이지별로 다른 보스와 비행기들을 만들 수 있다고 생각할 수 있다. 하지만 스테이지가 추가되거나 생성 규칙이 달라지는 등의 경우가 생길 경우, 코드 수정이 어렵고 복잡해진다. 이를 해결할 수 있는 패턴이 바로 추상 팩토리 패턴이다.

 

아래와 같이 추상 팩토리 클래스를 정의하고, 각각의 스테이지에 맞는 구체 클래스들을 구현해보자. 예제는 코틀린으로 작성하였다.

interface EnemyFactory {
    fun createBoss(): Boss
    fun createSmallFlight(): EnemyFlight
}
class EasyEnemyFactory : EnemyFactory {
    override fun createBoss(): Boss {
        return StrongAttackBoss(20, 10)
    }

    override fun createSmallFlight(): EnemyFlight {
        return DashSmallFlight(10, 5)
    }
}
class HardEnemyFactory : EnemyFactory {
    override fun createBoss(): Boss {
        return CloningBoss(40, 20)
    }

    override fun createSmallFlight(): EnemyFlight {
        return MissileSmallFlight(20, 10)
    }
}

 

 

이제 코드를 사용하는 클라이언트 쪽에서는, 복잡한 조건문 대신 알맞은 팩토리 클래스를 주입하면 된다. 생성 규칙이 변경되거나 추가되는 경우에는 팩토리 클래스만 수정해주면 된다.

class Stage(
    enemyFactory: EnemyFactory
) {
    val boss = enemyFactory.createBoss()
    val smallFlights = mutableListOf<EnemyFlight>().apply {
        repeat(10) {
            add(enemyFactory.createSmallFlight())
        }
    }
}
class StageTest {

    @Test
    fun create() {
        // when
        val easyStage = Stage(EasyEnemyFactory())

        // then
        assertTrue(easyStage.boss is StrongAttackBoss)
        assertTrue(easyStage.smallFlights.all { it is DashSmallFlight })

        // when
        val hardStage = Stage(HardEnemyFactory())

        // then
        assertTrue(hardStage.boss is CloningBoss)
        assertTrue(hardStage.smallFlights.all { it is MissileSmallFlight })
    }
}

 

 

우리가 흔히 볼 수 있는 코드 중에 추상 팩토리 패턴이 적용된 대표적인 예가 자바의 JDBC API이다. Connection 인터페이스는 Abstract Factory에 해당하고 Statement와 PreparedStatement는 Abstract Product에 해당한다. Concrete Factory와 Concrete Product는 각각의 DBMS Driver에서 구현한다. 따라서 클라이언트가 사용할 DBMS를 변경해야 할 경우, 클라이언트 코드의 수정 없이 Driver만 교체해주면 가능하게 된다.

 

 

이 글은 "객체 지향과 디자인 패턴 - 최범균" 책을 기반으로 작성되었다.

반응형

'디자인 패턴' 카테고리의 다른 글

파사드 패턴(Facade pattern)  (0) 2024.06.29
데코레이터 패턴(Decorator Pattern)  (0) 2024.06.24
컴포지트 패턴(Composite Pattern)  (0) 2024.06.23
어댑터 패턴(Adapter Pattern)  (0) 2024.06.21
반응형

1. JEP(JDK Enhancement Proposal) 목록

1.1 항상 엄격한 부동 소수점 (JEP 306)

이 JEP는 주로 과학 어플리케이션을 위한 것으로 부동 소수점 연산을 일관성 있게 엄격하게 만든다. 기본 부동 소수점 연산은 strict이거나 strictfp이며, 이 둘은 모든 플랫폼에서 부동 소수점 연산으로부터 동일한 결과를 보장한다.

자바 1.2 이전에는 strictfp 동작도 기본이었다. 하지만 하드웨어 문제로 인해 아키텍처가 바뀌었고 strictfp라는 키워드는 그러한 동작을 다시 활성화하기 위해 필요했다. 이제 더 이상 이 키워드를 사용할 필요가 없다.

 

1.2 향상된 Pseudo-Random Number Generators (JEP 356)

더 특별한 사용 사례와 관련된 JEP 356은 Pseudo-Random Number Generators(PRNG)를 위한 새로운 인터페이스 및 구현을 제공한다. 따라서 다양한 알고리즘을 상호 교환하여 사용하는 것이 더 쉽고 스트림 기반 프로그래밍에 대한 더 나은 지원을 제공한다.

public IntStream getPseudoInts(String algorithm, int streamSize) {
    // returns an IntStream with size @streamSize of random numbers generated using the @algorithm
    // where the lower bound is 0 and the upper is 100 (exclusive)
    return RandomGeneratorFactory.of(algorithm)
            .create()
            .ints(streamSize, 0,100);
}

 

java.util.Random, SplittableRandom and SecureRandom와 같은 레거시 random 클래스들은 이제 새로운 RandomGenerator 인터페이스를 구현한다.

 

1.3 새로운 macOS 렌더링 파이프라인 (JEP 382)

이 JEP는 애플이 Swing GUI에서 내부적으로 사용되는 OpenGL API (macOS 10.14)를 deprecate 처리 했기 때문에, macOS용 자바 2D 내부 렌더링 파이프라인을 제공한다. 새로운 구현은 애플 메탈 API를 사용하며 내부 엔진을 제외하고는 기존 API에 변경 사항이 없다.

 

1.4 macOS/AArch64 Port (JEP 391)

애플은 자사의 컴퓨터 라인을 X64에서 AArch64로 전환하는 장기 계획을 발표했다. 이 JEP는 맥OS 플랫폼에서 AArch64에서 실행되도록 JDK를 포팅한다.

 

1.5 Deprecate the Applet API for Removal (JEP 398)

Applet API를 이용해 개발 경력을 시작한 많은 자바 개발자들에게 이것은 슬플지 모르지만, 많은 웹 브라우저들은 이미 자바 플러그인에 대한 지원을 없앴다. API가 관련성이 없어지자, Java 9에 Deprecate되었던 이 API에 대한 for removal = true 표시를 했다.

 

1.6 강하게 캡슐화된 JDK Internals (JEP 403)

JEP 403은 –illegal-access

flag를 제거함으로써 JDK internals를 강하게 캡슐화하는데 한 단계 더 나아갔다. 플랫폼은 이 flag를 무시할 것이고, 만약 이 flag 값이 존재한다면 콘솔은 이 flag에 대한 사용 중단에 대한 메세지를 발행할 것이다.이 기능은 sun.misc.Unsafe와 같은 중요한 API를 제외하고 JDK internals에 사용자가 접근하는 것을 방지한다.

 

1.7 Switch 구문에 대한 패턴 매칭 (Preview) (JEP 406)

스위치 구문에 대한 패턴 매칭 기능이 추가되었다. 이로써 보일러플레이트 코드가 줄고 언어의 표현성이 향상되었다.

아래 예시를 통해 이해를 해보자.

static record Human (String name, int age, String profession) {}

public String checkObject(Object obj) {
    return switch (obj) {
        case Human h -> "Name: %s, age: %s and profession: %s".formatted(h.name(), h.age(), h.profession());
        case Circle c -> "This is a circle";
        case Shape s -> "It is just a shape";
        case null -> "It is null";
        default -> "It is an object";
    };
}

public String checkShape(Shape shape) {
    return switch (shape) {
        case Triangle t && (t.getNumberOfSides() != 3) -> "This is a weird triangle";
        case Circle c && (c.getNumberOfSides() != 0) -> "This is a weird circle";
        default -> "Just a normal shape";
    };
}

 

1.8 RMI Activation 제거 (JEP 407)

Java 15에서 for removal = true로 표시되었던 것이 Java 17에서 제거되었다.

 

1.9 Sealed class (JEP 409)

Java 15, 16에서 Preview 모드였던 Sealed class가 Java 17에서 공식적으로 언어의 새로운 기능이 되었다.

이 기능은 어떠한 클래스나 인터페이스가 이 Sealed class를 상속/구현할지의 여부를 제한한다. 새로운 Switch 구문의 패턴매칭과 함께 사용되면서 더 세련되고 깔끔한 타입 조사와 캐스팅이 가능하게 되었다. 아래 예시를 살펴보자.

int getNumberOfSides(Shape shape) {
    return switch (shape) {
        case WeirdTriangle t -> t.getNumberOfSides();
        case Circle c -> c.getNumberOfSides();
        case Triangle t -> t.getNumberOfSides();
        case Rectangle r -> r.getNumberOfSides();
        case Square s -> s.getNumberOfSides();
    };
}

 

1.10 AOT/JIT 컴파일러 제거 (JEP 410)

실험 기능으로 JDK 9와 JDK 10에 각각 도입된 AOT(Ahead-Of-Time) 컴파일러(JEP 295)와 GraalVM(JEP-317)의 JIT(Just-In-Time) 컴파일러는 유지 보수 비용이 높은 기능이었다.

이들은 별다른 채택을 받지 못했다. 이 때문에 이 JEP는 이들을 플랫폼에서 제외시켰지만, 개발자들은 여전히 그랄VM을 이용해 이들을 활용할 수 있다.

 

1.11 Deprecate the Security Manager for Removal (JEP 411)

Security Manager는 클라이언트 측 자바 코드를 보안하는 것을 목표로 했지만 더 이상 관련성이 없기 때문에 제거하도록 표시되었다.

 

1.12 Foreign Function and Memory API (Incubator) (JEP 412)

Foreign Function and Memory API를 통해 자바 개발자들은 JVM 외부에서 코드에 접근하여 힙 밖으로 메모리를 관리할 수 있다. JNI API를 대체하여 기존보다 보안성과 성능을 향상시키는 것이 목표이다.

이 API는 프로젝트 파나마에 의해 개발된 또 다른 기능이며, JEP 393, 389, 383 및 370에 의해 진화되고 선행되었다.

이 기능을 활용하여 우리는 Java class에서 C 라이브러리를 호출할 수 있다.

 

먼저 API를 통해 호출하고자 하는 라이브러리를 로드할 필요가 있다. 이후에 타겟 메서드 시그니처를 명시하고 호출한다.

public class JEP412 {
    private static final SymbolLookup libLookup;

    static {
        // loads a particular C library
        var path = JEP412.class.getResource("/print_name.so").getPath();
        System.load(path);
        libLookup = SymbolLookup.loaderLookup();
    }
    
    public String getPrintNameFormat(String name) {
        var printMethod = libLookup.lookup("printName");

        if (printMethod.isPresent()) {
            var methodReference = CLinker.getInstance()
                .downcallHandle(
                    printMethod.get(),
                    MethodType.methodType(MemoryAddress.class, MemoryAddress.class),
                    FunctionDescriptor.of(CLinker.C_POINTER, CLinker.C_POINTER)
                );

            try {
                var nativeString = CLinker.toCString(name, ResourceScope.newImplicitScope());
                var invokeReturn = methodReference.invoke(nativeString.address());
                var memoryAddress = (MemoryAddress) invokeReturn;
                return CLinker.toJavaString(memoryAddress);
            } catch (Throwable throwable) {
                throw new RuntimeException(throwable);
            }
        }
        throw new RuntimeException("printName function not found.");
    }
}

 

1.13 Vector API (Second Incubator) (JEP 414)

벡터 API는 병렬로 실행되는 다양한 명령어 집합을 의미하는 SIMD(Single Instruction, Multiple Data) 유형의 연산을 다룬다. 벡터 명령어를 지원하고 파이프라인과 같은 명령어의 실행을 허용하는 특수 CPU 하드웨어를 활용한다.
결과적으로, 새로운 API는 개발자들이 기본 하드웨어의 잠재력을 활용하여 보다 효율적인 코드를 구현할 수 있게 해줄 것이다.
이 연산에 대한 일상적인 사용 사례는 과학 대수 선형 응용, 이미지 처리, 문자 처리, 및 임의의 무거운 산술 응용 또는 다수의 독립 피연산자에 대한 연산을 적용해야 하는 임의의 응용이다.
API를 사용하여 간단한 벡터 곱셈 예를 설명해 보자

public void newVectorComputation(float[] a, float[] b, float[] c) {
    for (var i = 0; i < a.length; i += SPECIES.length()) {
        var m = SPECIES.indexInRange(i, a.length);
        var va = FloatVector.fromArray(SPECIES, a, i, m);
        var vb = FloatVector.fromArray(SPECIES, b, i, m);
        var vc = va.mul(vb);
        vc.intoArray(c, i, m);
    }
}

public void commonVectorComputation(float[] a, float[] b, float[] c) {
    for (var i = 0; i < a.length; i ++) {
        c[i] = a[i] * b[i];
    }
}

 

1.14 컨텍스트-특정적 역직렬화 필터 (JEP 415)

Java 9에 처음 도입된 JEP 290은 많은 보안 문제의 공통 소스인 신뢰할 수 없는 소스로부터 들어오는 직렬화된 데이터를 검증할 수 있게 해주었다. 그 검증은 JVM 레벨에서 이루어지며, 더 많은 보안과 견고성을 제공한다.
JEP 415로, 애플리케이션들은 JVM 레벨에서 정의된 컨텍스트-특정적이고 동적으로 선택된 병렬화 필터들을 구성할 수 있다. 각각의 병렬화 동작은 그러한 필터들을 호출할 것이다.

 

 

※ 참조

https://www.baeldung.com/java-17-new-features

반응형

'Java의 새로운 기능' 카테고리의 다른 글

Java 16의 새로운 기능  (2) 2024.06.10
Java 15의 새로운 기능  (0) 2024.06.09
Java 14의 새로운 기능  (0) 2024.06.07
Java 13의 새로운 기능  (1) 2024.06.06
Java 12의 새로운 기능  (0) 2024.06.05
반응형

1. Proxy 객체에서 default 메서드 호출

Java 16에서는 인터페이스의 default 메서드에 대한 개선된 기능으로 java.lang.reflect.InvocationHandler는 리플렉션을 사용하여 동적 프록시를 통해 인터페이스의 default 메서드를 호출한다.

아래에 간단한 예시를 살펴보자.

interface HelloWorld {
    default String hello() {
        return "world";
    }
}

 

이제 우리는 reflection을 사용하여 이 인터페이스의 프록시에 대한 default 메서드를 호출할 수 있다.

Object proxy = Proxy.newProxyInstance(getSystemClassLoader(), new Class<?>[] { HelloWorld.class },
    (prox, method, args) -> {
        if (method.isDefault()) {
            return InvocationHandler.invokeDefault(prox, method, args);
        }
        // ...
    }
);
Method method = proxy.getClass().getMethod("hello");
assertThat(method.invoke(proxy)).isEqualTo("world");

 

2. 새로운 시간 Symbol

DateTimeFormatter에 am/pm에 대한 대체 표현을 제공하는 새로운 symbol인 " B"가 추가되었다.

LocalTime date = LocalTime.parse("15:25:08.690791");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h B");
assertThat(date.format(formatter)).isEqualTo("3 in the afternoon");

 

우리는 "3 pm" 대신 "3 in the afternoon"을 출력값으로 얻을 수 있다. 또한 우리는 " B", " BBBB", " BBBBB" 등의 패턴을 통해 짧거나 전체로 이뤄진 스타일을 적용할 수 있다.

 

3. Stream.toList 메서드

Collectors.toList와 Collectors.toSet과 같은 Stream Collector로 이뤄진 보일러플레이트 코드들을 줄이기 위해 Stream.toList 메서드가 추가되었다. 아래 예시를 살펴보자.

List<String> integersAsString = Arrays.asList("1", "2", "3");
List<Integer> ints = integersAsString.stream().map(Integer::parseInt).collect(Collectors.toList());
List<Integer> intsEquivalent = integersAsString.stream().map(Integer::parseInt).toList();

 

예시에서 ints와 intsEquivalent는 동일한 결과 값을 가진다.

 

4. Vector API (인큐베이션 단계)

Java 16에서 Vector API는 초기 인큐베이션 단계에 있다. 이 API의 목적은 전통적인 스칼라 계산보다 최적화된 퍼포먼스를 내기 위해, 벡터 계산을 제공하는 것이다.

먼저 전통적인 방법으로 두 배열을 곱하는 예시를 살펴보자.

int[] a = {1, 2, 3, 4};
int[] b = {5, 6, 7, 8};

var c = new int[a.length];

for (int i = 0; i < a.length; i++) {
    c[i] = a[i] * b[i];
}

 

이 예시에서는 배열의 길이에 해당하는 4번의 cycle이 실행될 것이다. 이제 벡터 기반의 계산을 사용한 예시를 보자.

int[] a = {1, 2, 3, 4};
int[] b = {5, 6, 7, 8};

var vectorA = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
var vectorB = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
var vectorC = vectorA.mul(vectorB);
vectorC.intoArray(c, 0);

 

위 예시에서 우리가 처음으로 한 것은 fromArray 메서드를 통해 배열로부터 2개의 IntVector를 생성한 것이다. 메서드의 첫번째 파라미터는 벡터의 사이즈에 해당하고, 그 다음 파라미터는 배열, 그 다음은 offset 값이다. 여기서 가장 중요한 것은 벡터의 사이즈를 128 bit로 설정했다는 것이다. Java에서 각각의 int 값은 4 byte에 해당한다. 이 예시에서는 4개의 int를 가진 배열이 나오기 때문에, 이것을 저장하기 위해서는 128(4 X 8 X 4) bit가 필요하다. 여기서 단일 벡터는 전체 배열을 저장할 수 있다.

특정 아키텍처에서 컴파일러는 바이트 코드를 최적화하여 계산 과정을 4 -> 1 cycle로 줄일 수 있을 것이다. 이러한 최적화는 머신 러닝이나 암호화 프로세스에 유리하다.

Vector API는 인큐베이션 단계에 있는 것이기 때문에 Java의 새로운 릴리즈에서는 변경될 수 있다는 것을 기억해야 한다.

 

5. Record

Record는 Java 14에서 처음 소개되었다. Java 16은 여기에 점증적인 변화를 가져왔다.

Record는 제한된 class의 형태라는 점에서 enum과 비슷하다. Record를 정의하는 것은 불변 데이터 객체를 정의하는 간결한 방법이다.

 

5.1 Record가 없을 때

아래 예시에서 Book class를 정의해보자.

public final class Book {
    private final String title;
    private final String author;
    private final String isbn;

    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public String getIsbn() {
        return isbn;
    }

    @Override
    public boolean equals(Object o) {
        // ...
    }

    @Override
    public int hashCode() {
        return Objects.hash(title, author, isbn);
    }
}

 

Java에서 간단한 데이터 객체를 생성하는데는 많은 보일러플레이트 코드들이 필요하다. 이것은 다루기 번거롭고 때때로 개발자들이 equals와 hashCode와 같은 메서드들을 오버라이드하지 않아 버그를 발생시키기도 한다.

대부분의 요즘 IDE들은 getter, setter, 생성자 등과 같은 코드들에 대한 자동 생성 기능을 제공한다. 이것은 위에서 말했던 문제들을 경감시키고 개발자들의 부담을 덜어준다. 그러나 Record는 이러한 보일러플레이트 코드들 없이 똑같은 결과를 만들어 낸다.

 

5.2 Record가 있을 때

아래에 Record를 이용해서 다시 작성된 Book class를 보자.

public record Book(String title, String author, String isbn) {
}

 

record 키워드를 사용함으로써 Book class의 코드는 2줄로 줄어들었다. 이것은 훨씬 쉽고 에러가 발생시킬 확률이 적다.

 

5.3 Record에 대한 추가 사항

Java 16에서는 inner class의 멤버변수로 record를 사용하는 것이 가능하다.

class OuterClass {
    class InnerClass {
        Book book = new Book("Title", "author", "isbn");
    }
}

 

6. instanceof에 대한 패턴 매칭

instanceof에 대한 패턴 매칭은 Java 12에서 Preview 모드로 처음 소개되었다. Java 16에서 프로덕션 레벨로 올라온 것으로 보인다.

패턴 매칭이 없는 코드 예시는 아래와 같다.

Object obj = "TEST";

if (obj instanceof String) {
    String t = (String) obj;
    // do some logic...
}

 

패턴 매칭을 사용하여 아래와 같이 코드를 다시 작성할 수 있다.

Object obj = "TEST";

if (obj instanceof String t) {
    // do some logic
}

 

우리는 이제 instanceof check 과정의 일부로 변수(여기서는 t)를 선언할 수 있다.

 

7. Sealed class (Preview)

7.1 예제

인터페이스와 이를 구현하는 2개의 클래스를 예시로 살펴보자.

public sealed interface JungleAnimal permits Monkey, Snake  {
}

public final class Monkey implements JungleAnimal {
}

public non-sealed class Snake implements JungleAnimal {
}

 

sealed 키워드는 permits 키워드와 함께 사용되었는데, 이는 어떤 class들이 이 인터페이스를 구현하도록 허락되었는지를 결정해준다. 이 예제에서는 Monkey와 Snake이다.

sealed class의 모든 자식 class들은 반드시 아래의 것들 중 하나로 표시되어야 한다.

1) sealed : 어떤 class가 이 class를 상속하도록 허락되었는지를 permits 키워드를 사용하여 나타내야한다는 의미이다.

2) final : 추가적인 자식 class가 없다는 뜻이다.

3) non-sealed : 이 class는 어떤 클래스도 상속이 가능하다는 의미이다.

 

sealed class의 가장 큰 이점은 모든 경우의 수를 커버할 필요가 있는 패턴 매칭 체크를 완벽하게 해낼 수 있다는 것이다. 위에서 정의한 class로 예를 들면, 우리는 JungleAnimal의 자식 클래스에 대한 모든 가능한 경우를 커버할 수 있다.

JungleAnimal j = // some JungleAnimal instance

if (j instanceof Monkey m) {
    // do logic
} else if (j instanceof Snake s) {
    // do logic
}

 

여기서 sealed class는 오직 Monkey와 Snake만 자식 클래스로 허용하기 때문에, 추가적인 else 구문이 필요가 없다.

 

 

※ 참조

https://www.baeldung.com/java-16-new-features

반응형

'Java의 새로운 기능' 카테고리의 다른 글

Java 17의 새로운 기능  (1) 2024.06.16
Java 15의 새로운 기능  (0) 2024.06.09
Java 14의 새로운 기능  (0) 2024.06.07
Java 13의 새로운 기능  (1) 2024.06.06
Java 12의 새로운 기능  (0) 2024.06.05
반응형

1. Record (Preview)

record는 불변 데이터 객체를 더 쉽게 생성할 수 있게 해주는 새로운 타입의 자바 class이다.

Java 14에서 Preview모드로 소개되었으며, Java 15에서는 공식 Production 기능이 되기 전, 몇 가지를 더 개선하는 것을 목표로 하고 있다.

 

1.1 Record가 없을 때

record가 생기기 전에는 불변 데이터 전송 객체(DTO)를 다음과 같이 생성했을 것이다.

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

 

상태를 유지하는 불변 객체를 생성하기 위해 위와 같은 많은 코드가 필요하다. 모든 필드들은 명시적으로 final로 지정되어있고, 모든 필드를 파라미터로 가지는 한 개의 생성자를 가지고 있다. 또한 모든 필드들에 대한 getter가 있다. 때때로 우리는 subclass 생성을 막기 위해 class 자체를 final로 선언해야 한다.

대부분의 상황에서 우리는 한 단계 더 나아가 의미있는 로그를 출력하기 위해 toString() 메서드를 오버라이드한다. 또한 아마도 우리는 2개의 객체를 비교할 때 예측하지 못한 결과를 초래하지 않기 위해 equals()와 hashCode()메서드를 오버라이드 해야할 것이다.

 

1.2 Record를 사용할 때

record class를 사용함으로써, 우리는 똑같은 불변 객체를 훨씬 더 간편한 방법으로 정의할 수 있다.

public record Person(String name, int age) {
}

 

 

맨 앞에 위치한 class 정의는 Record에 대한 새로운 syntax이다. 이것은 record내의 필드들에 대한 정보를 제공한다.

또한 이를 통해 컴파일러는 class 내부의 필드를 추론할 수 있다. 이를 통해 우리는 멤버 변수나 getter와 같은 것들을 따로 정의할 필요가 없다. 또한 생성자도 정의할 필요가 없다.

게다가 컴파일러는 toString(), equals(), hashCode()에 대한 오버라이드된 메서드를 제공한다.

이처럼 record가 많은 보일러플레이트 코드를 삭제해주는 반면, 디폴트 동작을 오버라이드하는 것도 가능하다. 예를 들면 다음과 같이 생성자 내에 유효성 검사를 추가하는 것이 가능하다.

public record Person(String name, int age) {
    public Person {
        if(age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

 

record들은 언제나 final이다. 그러므로 abstact로 선언되거나 native 메서드를 사용할 수 없다.

 

2. Sealed Class (Preview)

Sealed class는 어떤 타입이 subclass로서 사용되는 것이 가능한지를 나타낼 수 있다. 이것은 인터페이스에도 적용되어 어떤 타입이 그것을 구현할 수 있을지를 결정할 수 있다.

Sealed class는 2가지 키워드로 구성된다. - sealed, permits

public abstract sealed class Person permits Employee, Manager {
    //...
}

 

위의 예제에서 우리는 Person이라는 class를 abstract로 선언하였다. 그리고 우리는 오직 Employee, Manager class만이 Person class를 상속하는 것이 가능하다는 것을 명시하였다. 아래는 Person class를 상속한 예시이다.

public final class Employee extends Person {
}

public non-sealed class Manager extends Person {
}

 

sealed class를 상속하는 class는 반드시 sealed나 non-sealed 혹은 final로 선언되어야 한다. 이것은 class의 계층구조를 유한하게 유지시켜 준다.

이 유한한 계층구조는 sealed class를 사용하는 장점 중의 하나이다. 아래 예시를 보자.

if (person instanceof Employee) {
    return ((Employee) person).getEmployeeId();
} 
else if (person instanceof Manager) {
    return ((Manager) person).getSupervisorId();
}

 

sealed class가 없다면, 컴파일러는 모든 subclass가 커버되었는지를 if-else문을 통해 알 수 없을 것이다. 컴파일러는 else 구문이 없다면, 이 로직이 모든 케이스를 커버하지 못한다는 경고를 할 것이다.

 

3. 기타 변경 사항

Java 15에서는 Java 13과 14에서 Preview 모드로 제공되었던 텍스트 블록이 Production 기능으로 제공된다.

또한 Java 14에서 제공되었던 유용한 NullpointerException 기능이 디폴트로 제공된다.

 

 

※ 참조

https://www.baeldung.com/java-15-new

 

반응형

'Java의 새로운 기능' 카테고리의 다른 글

Java 17의 새로운 기능  (1) 2024.06.16
Java 16의 새로운 기능  (2) 2024.06.10
Java 14의 새로운 기능  (0) 2024.06.07
Java 13의 새로운 기능  (1) 2024.06.06
Java 12의 새로운 기능  (0) 2024.06.05

+ Recent posts