반응형
예제에 대한 전체 코드는 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

+ Recent posts