본문 바로가기

프로그래밍/ect

SOLID, 객체 지향 설계 5원칙 : 단일 책임 원칙(Single Responsibility Principle)

By Angelacleancoder - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=88628927

 

객체 지향 설계 원칙이란, 객체 지향의 4가지 특성인 캡슐화, 상속, 추상화, 다형성을 이용하여 올바르게 설계하는 것을 도와주는 원칙을 말한다.

 

좋은 소프트웨어는 자기 자신 안의 클래스 응집도는 높이고, 타 클래스 간 결합도는 낮추는 High Cohesion - Loose Coupling 원칙을 가지고 있다는 것을 객체 지향 관점에서 도입한 것이다.

 

이렇게 설계된 소프트웨어는 재사용성이 높고, 수정이 최소화되어 결국 유지 보수가 용이해진다는 특징이 있다.

 

SRP (Single Responsibility Principle) : 단일 책임 원칙

'하나의 모듈(클래스나 클래스의 모음)는 하나의 책임만을 가져야 한다.' 는 원칙을 말한다.

 

SRP에서의 책임은 하나의 기능이라고 생각하면 된다.

 

SRP 준수 여부의 가장 큰 척도는 기능 변경 시의 파급 효과이다.

 

하나의 모듈에서 책임이 많아지면, 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아져 시스템이 복잡해질 수 있다.

 

예를 들어 A를 고쳤더니 B를 고쳐야 하고, B를 고쳤더니 C를 수정해야 하고, C를 수정했더니 다시 A를 수정해야 하는, 책임이 순환되는 형태가 될 수 있다.

 

이처럼 하나의 모듈에서 책임이 많아지면 한 책임의 변경에서 다른 책임의 변경으로 연쇄작용이 일어날 수 있다.

 

단일 책임 원칙을 적절히 적용했다면, 모듈 마다 알맞은 책임을 가져 A 또는 B, C만 수정하면 해결될 것이다.

 

이를 다르게 표현하면, '모듈이 변경되는 이유가 한가지여야 한다.'라고 할 수도 있다.

 

하나의 모듈 여러 액터에 대해 책임을 가지고 있다면, 여러 액터들에게 변경 요구가 올 수 있어, 해당 모듈을 수정해야 하는 이유가 여러 개가 될 수 있다. 

 

👨‍👩‍👧‍👦 액터 : 시스템과 상호작용 하는 사용자를 말하는데, 사용자는 사람, 조직, 머신이나 외부 시스템이 될 수도 있다.

 

하지만, 어떤 모듈이 특정 액터에 대해서만 책임을 가지고 있다면, 모듈 변경 이유와 시점이 명확해진다.

 

예를 들어, 사용자에게 아이디와 비밀번호를 입력받아 비밀번호를 암호화하여 DB에 저장하는 로직이 있다고 하자.

 

class UserService(private val userRepository: UserRepository) {

    fun addUser(email: String, pw: String) {
        val sb = StringBuilder()

        for (b in pw.toByteArray(Charsets.UTF_8)) {
            sb.append(((b.toInt() and 0xff) + 0x100).toString(16).substring(1))
        }

        val encryptedPassword = sb.toString()
        val user = User.builder()
            .email(email)
            .pw(encryptedPassword)
            .build()

        userRepository.save(user)
    }
}

 

UserService는 사용자의 정보를 입력 받고, 비밀번호를 암호화 하는 2가지의 책임을 가지고 있다. 

 

따라서 다음과 같은 2가지 다른 로직 변경 이유가 발생할 수 있다.

  • 기획팀 : 사용자를 추가할 때, 아이디와 비밀번호 말고 이메일도 추가로 입력 받게 해주세요.
  • 보안팀 : 비밀번호 암호화 방식을 개선할겁니다.

UserService 클래스는 기획팀, 보안팀 2개의 액터에 대한 책임을 분리해야 한다.

 

비밀번호 암호화는 SimplePasswordEncoder 클래스를 만들어 책임을 분리하고,

 

UserService로부터 이를 추상화하여 사용하면 비밀번호 암호화 방식 개선이라는 변경 사항을 분리할 수 있다. 

class SimplePasswordEncoder{

    fun encryptPassword(pw : String) : String {
    	val sb = StringBuilder()

        for (b in pw.toByteArray(Charsets.UTF_8)) {
            sb.append(((b.toInt() and 0xff) + 0x100).toString(16).substring(1))
        }
        
        return sb.toString()
    }
}

 

class UserService(private val userRepository : UserRepository, 
                  private val passwordEncoder : SimplePasswordEncoder) {

    fun addUser(email: String, pw: String) {

        val encryptedPassword = passwordEncoder.encryptPassword(pw)
        
        val user = User.builder()
            .email(email)
            .pw(encryptedPassword)
            .build()

        userRepository.save(user)
    }
}

 

이처럼 단일 책임 원칙을 준수하여 책임과 관심이 다른 코드들을 적절히 분리하고, 결합도를 낮춤으로써 시스템 변화에 대해 좀 더 쉽게 대응할 수 있다.