Study/object

[엘레강트 오브젝트] 2-5장 퍼블릭 상수(Public Constant)를 사용하지 마세요

에디개발자 2021. 8. 21. 09:52
반응형

나를 닮았다고 한다...

이 글은 엘레강트 오브젝트 새로운 관점에서 바라본 객체지향 도서를 보며 스터디한 글입니다.

책에서 주장하는 내용을 정리하였으며 예제들은 모두 코틀린 코드로 변환하여 작성하였습니다.

 

목차

  1. 퍼블릭 상수
  2. 결합도 증가
  3. 응집도 저하
  4. 개선
  5. 클래스의 단위를 최소화하자
  6. 결론
  7. 주관적인 생각

1. 퍼블릭 상수

상수(Constant) 는 public static final 로 선언되며 객체 사이에서 데이터를 공유하기 위해 사용되는 메커니즘입니다. 하지만 객체지향에서는 객체들 간의 어떤 것도 공유해서는 안됩니다. 예제를 통해 살펴보겠습니다.

 

올바른 예

class Records(
    private val records: MutableList<Record>
) {
    companion object {
        private const val EOL = "\r\n"
    }
    
    fun all() = records
    
    fun write(out: Writer) {
        for (record in this.all()) {
            out.write(record.toString())
            out.write(Records.EOL)
        }
    }
}

class Record(
    val rec: String
)
kotlin에서 const val 는 컴파일 시간에 결정되는 상수

Records라는 클래스가 있고 내부의 EOL이라는 상수를 사용하여 로직을 처리하고 있습니다. 이 경우 EOL 상수는 클래스 내부에서만 사용할 수 있는 private이므로 EOL 상수는 외부 객체에서 사용할 수 없습니다. 이 경우 매우 올바른 클래스 설계입니다.

 

비슷한 동작을 하는 다른 클래스를 생성해보겠습니다.

class Rows(
    private val rows: MutableList<Row>
) {
    companion object {
        private const val EOL = "\r\n"
    }
    
    private fun fetch() = rows
    
    fun print(pnt: PrintStream) {
        for (row in this.fetch()) {
            pnt.print("{ $row }${Rows.EOL}")
        }
    }
}

class Row(
    val r: String
)

이 클래스 또한 내부에서 상수를 사용하고 내부에서만 사용할 수 있도록 private을 사용하여 클래스 설계가 잘 되어있습니다. 

 

문제의 시작

두 개의 클래스를 생성하고 보니 동일한 상수를 2개의 클래스에서 사용하고 있습니다. 그렇다면 상수로 빼서 두 클래스에서 사용하는 순간 문제가 발생합니다. 

// 중복 발생하는 EOL을 const 객체를 이용하여 공통처리
class Constants {
    companion object {
        const val EOL = "\r\n"
    }
}

 

 

2. 결합도 증가

Constant 객체로 공통처리한 후 변경된 두 클래스입니다.

class Records {
    fun write(out: Writer) {
        for (record in this.all()) {
            out.write(record.toString())
            out.write(Constants.EOL)   // 적용
        }
    }
}

class Rows {
    fun print(pnt: PrintStream) {
        for (row in this.fetch()) {
            pnt.print("{ $row }${Constants.EOL}")  // 적용
        }
    }
}

Recods, Rows 클래스는 Constants 객체와 결합되어 있습니다. Constants 객체에서 점점 더 많은 상수를 선언하면서 다른 객체들과 결합된다고 가정해보겠습니다. 추 후 유지보수를 할 때 Constants의 상수값을 변경해야할 일에 마주쳤을 때 쉽게 수정할 수 없을 것 입니다. 

 

3. 응집도 저하

퍼블릭 상수를 사용한다면 객체의 응집도는 낮아집니다. 응집도가 낮아진다면 객체가 자신의 문제를 해결하는 능력이 저하된다는 것을 의미합니다. 퍼블릭 상수는 하드코딩되어있는 텍스트 객체일 뿐입니다. 이 책에서 주장하는 객체는 자신의 문제를 해결해야하지만 이 상수는 아무런 행위를 하지 않고 텍스트만 반환합니다. 

응집도란 객체안의 요소들이 서로 관련되어 있는 정도

 

4. 개선

퍼블릭 상수를 사용하기 보다는 하나의 객체를 생성하여 사용하는 방법입니다. 문제가 되었던 클래스를 수정해보겠습니다.

 

먼저 공통 코드를 처리하는 클래스를 생성합니다.

class EOLString(
    private val origin: String
) {
    override fun toString(): String {
        return String.format("%s\r\n", this.origin)
    }
}

 

위 클래스를 사용하도록 변경합니다.

class Records {
    fun write(out: Writer) {
        for (record in this.all()) {
            out.write("${EOLString(record.toString())}")
        }
    }
}

class Rows {
    fun print(pnt: PrintStream) {
        for (row in this.fetch()) {
            pnt.print("${EOLString("{ $row }")}")
        }
    }
}

 

변경 후 유지보수 하던 중 특정 경우에서는 사용하지 못하는 케이스가 발생했을 경우 EOLString 클래스만 수정하면 해결됩니다.

class EOLString(
    private val origin: String
) {
    override fun toString(): String {
        if ( /* EOL을 사용할 수 없는 경우 */ ) {
            throw IllegalStateException("EOL을 사용할 수 없습니다.")
        }
        return String.format("%s\r\n", this.origin)
    }
}
만약에 이와 같은 문제를 Constants.EOL을 사용했다면 어떻게 해야할까요??
Constants.EOL을 사용하는 모든 클래스에 찾아가 if 달고 예외처리를 해야하는 불편한 사항이 벌어질 것입니다.

 

5. 클래스 단위를 최소화하자

클래스 단위를 최소화해야 설계가 좋아지고 유지보수가 편해집니다. 

 

일상생활에서 비유를 해보자

작은 클래스 단위

내 고양이는 생선을 먹고 우유를 마시는 것을 좋아한다.

여기서 객체는 고양이, 생선, 우유 정도가 될 것입니다. 고양이는 먹고, 마시는 행위를 취할 수 있습니다. 

 

큰 클래스 단위

내 것은 그것을 먹고 다른 것을 마시는 것을 좋아한다.

여기서 객체는 내 것, 그것, 다른 것 일 것입니다. 만약에 고양이가 아닌 다른 동물들이 추가되고 여러가지 음식이 추가된다면 클래스에 더 많은 기능들이 생길 것이며 클래스는 점점 커질 것 입니다.

 

프로그래밍 예시

큰 클래스 단위

val body = HttpRequest()
    .method("POST")
    .fetch()

 

작은 클래스 단위

val body = PostRequest(HttpRequest())
    .fetch()

 

6. 결론

퍼블릭 상수를 이용해서 코드 중복 문제를 해결하지 말고 대신 클래스를 사용하자.

 


7. 주관적인 생각

이 책에서 주장하는 퍼블릭 상수에 대한 대부분 내용에는 동의합니다. 하지만 절대 쓰지않는 것에는 동의하지 않습니다. 로직에서는 상수형을 사용하면 위와 같은 문제가 발생할 수 있지만 저는 @Bean Value를 선언할 때 퍼블릭 상수를 사용합니다. 

 

실제 사용하는 예시로는 특히 스프링 배치에서 Job, Step, Reader 의 Bean Value를 선언할 때 사용하고 있습니다. 

@Bean("${BatchItem.SIMPLE}_JOB")
fun simpleJob() =
    jobBuilderFactory.get("${BatchItem.SIMPLE}_JOB")
        .start(simpleStep(null))
        .build()

@Bean("${BatchItem.SIMPLE}_STEP")
@JobScope
fun simpleStep(@Value("#{jobParameters[chunkSize]}") chunkSize: Int?) =
    stepBuilderFactory.get("${BatchItem.SIMPLE}_STEP")
        .chunk<Person, Person>(chunkSize!!)
        .reader(reader(null))
        .processor(simpleProcessor)
        .writer(simpleWriter)
        .listener(simpleStepListener)
        .build()

@Bean("${BatchItem.SIMPLE}_READER")
@StepScope
fun reader(@Value("#{jobParameters[pageSize]}") pageSize: Int?): QuerydslPagingItemReader<Person> {
    val reader = QuerydslPagingItemReader(entityManagerFactory) { personRepository.findAllInBatch() }
    reader.pageSize = pageSize!!
    reader.pageOffset = false
    return reader
}

 

이 책의 내용이나 제 생각과 다른 의견이 있으시면 댓글을 달아주세요. :)



반응형