상태 패턴(Status Pattern) - 상태 추가/삭제에 고통받았던 나에게

CURE·2025년 1월 7일
0

목록 보기
4/6

Intro


A: 이번에 새로운 상태를 추가해야할 것같아요
B: 아...언제 다고치지...

상태를 추가할 때마다 고통받았던 지난날이 생각나서 글을 작성하게 되었습니다 ㅠ


상태패턴을 쓰기 전

처음 상태패턴을 알기 전에는 상태라는 상태 표현을 단순한 문자열, Enum, 객체 또는
리스트로 관리를 했었다.

// 일관된 객체로 저장
const userStatus = {
	'ACTIVATE': 'ACTIVATE',
  	'LOGIN': 'LOGIN',
  	'BLOCK':'BLOCK',
  	'DEACTIVATE': 'DEACTIVATE',
}

그렇기 때문에 user의 상태에 따라 예외 처리도 각각 해줘야 했었다

// 문자열 
class User {
  
	status: string
    deletedAt: DATE|null
    
	activate(){
      	// 각각 상태별 if 처리 
      	if(this.status === userStatus.ACTIVATE){
        	throw new Error('탈퇴한 회원 입니다!')
        } else if(this.status === userStatus.LOGIN){
         	// login 상태 일 때
        } else if(this.status === userStatus.BLOCK){
        	// block 상태 일 때
        } else if(this.status === userStatus.DEACTIVATE){
			// 탈퇴 유저 일 때
        } else {
        	throw new Error('존재하지 않은 상태입니다)
        }
      	
        // 로직 처리
    }

	// 로그인
	login(){
      	// 각각 상태별 if 처리 
      	if(this.status === userStatus.ACTIVATE){
        	throw new Error('탈퇴한 회원 입니다!')
        } else if(this.status === userStatus.LOGIN){
         	// login 상태 일 때
        } else if(this.status === userStatus.BLOCK){
        	// block 상태 일 때
        } else if(this.status === userStatus.DEACTIVATE){
			// 탈퇴 유저 일 때
        } else {
        	throw new Error('존재하지 않은 상태입니다)
        }
      	
        // 로직 처리
    }

   	block(){
      
        // 각각 상태별 if 처리 
      	if(this.status === userStatus.ACTIVATE){
        	throw new Error('탈퇴한 회원 입니다!')
        } else if(this.status === userStatus.LOGIN){
         	// login 상태 일 때
        } else if(this.status === userStatus.BLOCK){
        	// block 상태 일 때
        } else if(this.status === userStatus.DEACTIVATE){
			// 탈퇴 유저 일 때
        } else {
        	throw new Error('존재하지 않은 상태입니다)
        }
      	
        // 로직 처리
    	
    }
}


// 비지니스 로그인 로직
const login = (id: number)=>{
  
  	const user = otherService.getUser(id)
 
    // 어떤 값을 낼지 모르는 user.status 에 대해 
    // login 함수에서는 status 에 대한 분기 필요
  	user.login();
}

혹여라도 if 하나가 빠지거나 예외를 처리하지 않았을 경우, 해당 부분에서 에러가 나는 원인이 되기도 했다.

또한 새로운 상태가 추가될 때 부터 머리가 아파오기 시작한다.

만약에...

예를 들어 위의 상황에서 최근 로그인 유저 라는 상태가 추가된다면 아마 이렇게 수정될 것이다.

// 새로운 사태 추가
const userStatus = {
	'ACTIVATE': 'ACTIVATE',
  	'LOGIN': 'LOGIN',
  	'BLOCK':'BLOCK',
  	'DEACTIVATE': 'DEACTIVATE',
  	'RECENT': 'RECENT' // 새로운 상태 추가
}

그리고 최근 로그인 유저는 비밀번호를 변경하라는 알람을 줘야한다면


class User {
  
	status: string
    deletedAt: DATE|null
    
	activate(){
    	// 생략
    }

	// 로그인
	login(){
      	// 각각 상태별 if 처리 
      	if(this.status === userStatus.ACTIVATE){
        	throw new Error('탈퇴한 회원 입니다!')
        } else if(this.status === userStatus.LOGIN){
         	// login 상태 일 때
        } else if(this.status === userStatus.BLOCK){
        	// block 상태 일 때
        } else if(this.status === userStatus.DEACTIVATE){
			// 탈퇴 유저 일 때
        } else if(this.status === userStatus.RECENT){
          	// 새로 운 상태 추가 
			messageSend();
        } else {
        	throw new Error('존재하지 않은 상태입니다)
        }
      	
        // 로직 처리
    }

   	block(){
        // 생략	
    }
}

이때의 문제점은 바로 상태가 추가할 때마다 상태에 대한 예외처리를 해야한 다는 것이다.

그것도 모든 부분에 말이다.

login 함수 뿐 만 아니라 activate(), block() 함수에서도 마찬가지이다.

이런걸 해결하고자 조금 상태의 형식을 바꿔봐도 어쩔 수 없이 비지니스로직에서는 해당 조건을 제거할 수가 없었다.

이때 알게된 상태 패턴...!

더 뾰족한 수가 없어 가만히 있던 와중에
최근에 다시 읽게 된 헤드퍼스트 디자인 패턴에서
눈길이 가는 패턴이 있었다.

바로 이글의 제목처럼 상태 패턴이라는 것인데

지금처럼 쓰고있는 부분을 객체로 나누어 관리하는 패턴이다.

해당 패턴은 상태에 대한 변경을 각 상태 클래스에게 위임하는 행위로

내부적으로 변경되는 상태는 하위 객체가 책임을 갖는 것을 의미한다.

일단 어떻게 쓰는데...?

만약 지금처럼의 구조를 상태 패턴으로 수정하고자 한다면

우선 상태에 대한 interface 를 만들고

if 분기점이 필요한 부분만 interface 함수로 만든다.

interface userStatus{
	activate(user: User):void
  	login(user: User):void
    block(user: User):void   
}
    

해당 상태에 맞는 구현체들을 구현한다.

class ActivateStatus implements userStatus{
	activate(user: User):void{
		// activate 상태일때 activate 호출시
      	throw new Error('이미 상태 완료입니다.')
    }

 	login(user: User):void{
    	// activate 상태일때 login 호출시
      	
    }

	block(user: User):void{
    	// activate 상태일때 block 호출 시
    }
}

기존의 있던 클래스를 수정한다.

class User {
  	// 바꾸고자 하는 상태를 주입
	status: UserStatus
    
    activate(){
    	this.status.activate(this)
    }

	login(){
    	this.status.login(this)
    }

	block(){
    	this.status.block(this)
    }
 
}

이렇게 되면 status 에 의해 변하게 되는 if 조건 추가들은 각 클래스의 함수가 실행이 되는 것이고 이는 각각 내부 구현체에서 실행하게 된다.

다시 이전 상황처럼 새로운 상태가 추가된다 하더라도

단순히 userStatus와 그에 따른 구현체만 추가하면 쉽게 추가할 수 있다.


class RecentStatus implements userStatus{
	activate(user: User):void{
		// recent 상태일때 activate 호출시
    }

 	login(user: User):void{
    	// recent 상태일때 login 호출시
      	// 비밀번호 변경 알림
      	sendMessage();
      	
    }

	block(user: User):void{
    	// recent 상태일때 block 호출 시
    }
}

또한 기존의 있던 구현체에서 상황에 맞게 끔 수정해도

if문 전체를 확인하는 일은 없어질 것이다.


상태패턴 정의

상태 패턴은 객체가 특정 상황에서 행위를 달리할 때, 상태를 조건문이아닌 객체화하여 상태가 행동을 할 수 있도로 위임하는 패턴을 말한다.

클래스 다이어그램

상태 패턴 개인 정리

상태패턴이란?
상태에 따라 어떤 메소드를 실행할 때 분기점을 나눠야할 때 쓰기 좋은 패턴

해당 패턴을 통해 각 얻을 수 있는 장점은

  • 각 상태마다 어떤 메소드에서 어떤 에러가 나야하는지 가독성이 좋아진다.
  • 새로운 상태가 추가 되어도 기존 코드를 그대로 유지할 수 있다

상태패턴을 오인 했던 점

상태 패턴이니 상태로 interface 만 묶고 어떤 행동을 해야하는지 적으면 되는것 아닌가? -> X

어떤 행동이라는 것이 꼭 동반되어야한다.

그래야 상태에 따라 구분해서 행동하니말이다.

Outro


패턴들을 하나씩 이해할 때마다 객체지향에 대한 매력이 점점 커지는 것 같다...!

그동안 절차지향으로만 서비스를 유지보수 해왔는데 이런 패턴들을 잘 적용한다면

유지보수가 더 빨라졌을 거라 앞으로가 기대가 너무 된다.

profile
Full 이 되고 싶은 BackEnd

0개의 댓글

관련 채용 정보