반응형

OOP가 무엇일까?

Object-Oriented Programming의 약자로 객체지향 프로그래밍이란 뜻이다.

프로그래밍의 종류에는 절차지향, 객체지향, 함수지향이 있다. (더 있을 수 있는데 여기까지..)

절차지향은 말그대로 위에서부터 주우욱 읽어가는 코드로 C언어 등이 있다.(요즘은 구조체로 객체의 느낌이 나지만 자세히는 모름)

객체지향으론 유명하게 파이썬과 java가 있다. 객체를 구성해서 프로그래밍하는 것인데 class를 만드는 것이다.

함수지향은 자료처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임이라한다. 자바스크립트 등이 있다.(잘 모르겠다...)

일반적으로는 객체지향을 잘 하면된다.- 제일 많이 쓰이니까!

객체지향이란 것은 객체를 생성해서 프로그래밍한다는 뜻인데 여기에는 룰이 존재한다. 그것이 5대 원칙이다. 지켰으면 좋겠다...라는 뜻

서론은 여기까지 하고, 5대원칙에 대해 설명해보자.

5대 원칙은 약자로 SOLID라고 하는데 각각 SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), DIP(의존 역전 원칙), ISP(인터페이스 분리 원칙)의 앞글자를 따서 만들어졌다. 

각각에 대해 알아보자.

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

하나의 모듈은 한 가지 책임을 가져야한다는 것이다. 다른말로 작성된 Class는 하나의 기능만 가지며, 그 Class가 제공하는 모든 서비스는 하나의 책임을 수행하는 데 집중되어야한다는 것이다. 

예를 들어보자. 회원가입의 기능을 하는 SignUp class가 존재한다고 하자. SignUp 클래스 안에는 회원 가입을 담당하는 기능만 존재해야 한다. (로그인이나, 유저 삭제 등의 기능이 들어가면 안된다.) SignUp 클래스를 고쳤을 때 회원가입 기능만 문제가 발생하여야한다. 

만약 단일 책임 원칙을 지키지 않았을 경우에는 하나의 class에 다양한 기능이 들어가 있다는 뜻인데, 해당 클래스를 수정했을 때 다른 모듈에 어떠한 영향을 미치는지 그 범위를 추측하기 힘들 수 있다.

아래의 예를 보면 유저를 입력하는 메소드에 비밀번호를 암호화하는 코드가 SignUp 클래스 안에 혼재해 있다. 비밀번호 암호화 코드를 다른 클래스로 빼자. 

class SignUp:
    def insertUser(self, userId, userPw):
        ...
        #something insert user to DB code
        ...
        #something password encrypt code
		...
class SignUp:
	SimplePasswordEncoder simple_password_encoder
    
	def insertUser(self, userId, userPw):
        ...
        #something insert user to DB code
        ...
        
 class SimplePasswordEncoder:
 	def encryptPassword(pw):
    	...

위와 같이 클래스단위로 구분했는데, 그럼 왜 SingIn 클래스에 암호화 메소드를 추가하지 않느냐? 라는 의문이 든다. 나도 그렇게 생각했는데 이것이 바로 단일 책임이라는 것이다. 클래스(객체)가 단일 책임만 갖는 것이다.

단일 책임의 장점은 변경이 필요할 때 수정할 대상이 명확하다는 것이다.

개방 폐쇄 원칙 (Open-Closed Principle, OCP)

확장에 대해 열려있고 수정에 대해서는 닫혀있어야 한다는 원칙으로, 각각이 갖는 의미는 다음과 같다.

  • 확장에 대해 열려있다: 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
  • 수정에 대해 닫혀있다: 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.

내 상사가 나한테 암호화 알고리즘의 고도화가 필요하다고 요청을 한다. 그럼 한 번 위의 코드에서 바꿔보자.

class SignUp:
	ComplexPasswordEncoder complex_password_encoder
    
	def insertUser(self, userId, userPw):
        ...
        #something insert user to DB code
        ...
        
 class ComplexPasswordEncoder:
 	def encryptPassword(pw):
    	...
        #something powerful encrypt code
        ...

SimplePasswordEncoder를 ComplexPasswordEncoder로 수정했다. 그런데, 기존의 코드였던, 그리고 암호화 정책과 무관한 SignIn 코드도 고쳐야만 했다.

이 부분은 수정에 대해 닫혀있다라는 원칙에 위배된다. 이 부분을 해결하려면 추상화를 이용해야한다.

추상화 관련 내용은 이 링크를 따라가자

내가 이해한 내용을 바탕으로 예제를 보자.

from abc import *

class SignUp:
	PasswordEncoder password_encoder
    
	def insertUser(self, userId, userPw):
        ...
        #something insert user to DB code
        ...
        
class PowerPWEncoder(metaclass=ABCMeta):
	@abstractmethod
    def encryptPassword(self, pw):
    	...
        #something powerful encrypt code
        ...

class PasswordEncoder(PowerPWEncoder):
	def encryptPassword(self, pw):
	   	return super().encryptPassword()

위와 같은 방식을 이용하면 상속 클래스만 바꿔도 패스워드 암호화 방식을 변경할 수 있다. 파이썬에서 인터페이스가 따로 존재하지 않으므로 위 코드에서 PasswordEncoder 클래스를 인터페이스 클래스로 사용하였다. 

 

인터페이스 분리 원칙(Interface Segregation Principle, ISP)

객체가 충분히 높은 응집도의 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 적절하게 분리해야한다. 즉, 인터페이스 분리원칙이란 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것이다.

인터페이스 분리 원칙을 준수하면 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스만을 접근하여 불필요한 간섭을 최소화할 수 있다. 

위 코드에서 사용자가 비밀번호를 변경할 때 입력한 비밀번호가 기존의 비밀번호와 동일한지 여부를 판단해야하는 클라이언트가 있다고 생각해보자. 이때 PowerPWEncoder 클래스에서 encryptPassword를 동일하게 사용해줘야한다. 이유는 이미 기존의 비밀번호들은 강화된 암호화를 이용해 저장되어 있기 때문에 rawPW를 입력받아 강화된 암호화를 적용해서 기존의 암호와 비교해야하기 때문이다.

 

from abc import *

class SignUp:
	PasswordEncoder password_encoder
    
	def insertUser(self, userId, userPw):
        ...
        #something insert user to DB code
        ...
     
        
class PowerPWEncoder(metaclass=ABCMeta):
	@abstractmethod
    def encryptPassword(self, pw):
    	...
        #something powerful encrypt code
        ...
  
class PowerPWChecker(metaclass=ABCMeta):
	@abstractmethod
    def encryptPassword(self, pw):
    	...
        #something powerful encrypt code
        ...
    
    @abstractmethod
    def isCorrectPassword(self, rawPW, pw):
    	pass

class PasswordEncoder(PowerPWEncoder):
	def encryptPassword(self, pw):
	   	return super().encryptPassword()
        

        
class PasswordChecker(PowerPWChecker):
	def encryptPassword(self, pw):
	   	return super().encryptPassword()
    
    def isCorrectPassword(self, rawPW, pw):
    	encryptPW = encryptPassword(rawPW)
        return encryptPW.equals(pw).

이렇듯 클라이언트가 원하는 대로 인터페이스를 분리하여 불필요한 접근을 최소화해줄 수 있다.

 

리스코프 치환 원칙(Liskov Substitution Principle, LSP)

1988년 바바라 리스코프가 올바른 상속관계의 특징을 정의하기 위해 발표한 것이다. 하위 타입은 상위 타입을 대체할 수 있어야 한다는 것이다. 즉, 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경되어도, 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다는 것이다.

두 가지를 지키면 된다고 한다.

  1. 하위 클래스는 상위 클래스에 정의된 것보다 사전조건을 엄격하게 만들면 안된다.
  2. 하위 클래스는 상위 클래스에 정의된 것 보다 약한 사후조건을 만들면 안된다.

하위 클래스의 메소드에 특정 변수가 필요한데, 상위 클래스의 메소드에 그 변수가 없을 경우 에러가 남.

상위 클래스의 메소드를 하위 클래스가 오버라이딩 할 경우 메소드의 기능은 동일해야함.

리스코프 치환 원칙을 준수하려면 상위 클래스의 메소드를 그대로 상속 받고 하위 클래스에서 필요한 기능을 추가하는 방향으로 가는 것이 좋다.

 

의존 역전 원칙 (Dependency Inversion Principle, DIP)

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되며, 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다는 것이다.

고수준 모듈: 변경이 없는 추상화된 클래스(또는 인터페이스)

저수준 모듈: 변하기 쉬운 구체 클래스

다시 말하자면 메인 스트림이 고수준의 모듈인 인터페이스에 의존되고 인터페이스는 구현체에 의존되어야 한다는 것이다.

개방폐쇄원칙과 밀접한 관련이 있으며 의존 역전 원칙이 위배되면 개방 폐쇄 원칙 역시 위배되게 될 가능성이 높다.

 

위의 SOLID는 추상화의 중요성을 말하고 있는 듯하다. 추상 클래스를 잘 활용하여 확장, 수정이 용이한 객체 지향적인 코드를 프로그래밍해보자.


반응형

'Python' 카테고리의 다른 글

"파이썬으로 데이터 분석하기: 주식 시장 예측 모델링"  (0) 2023.04.19
디자인 패턴  (0) 2022.04.14
추상클래스(abstract class)  (0) 2022.04.09
덕 타이핑이란?  (0) 2022.04.09
opencv cv2.imshow 에러 해결방법  (0) 2022.03.31

+ Recent posts