계약과 문서 with Javadoc

윤영로·2025년 5월 29일

개발관리

목록 보기
3/3

목표

  • 계약에 의한 설계에 따른 Operation 문서화 방법
  • Javadoc 문서 Convention 정리

용어 정의

용어설명
추이적 계약(transitive contract)A -> B, B -> C의 Operations가 있을 때, B -> C의 계약이 A에게 노출되는 것

배경

계약에 의한 설계(Design By Contract)에 의하면 모든 공개된 연산(public operation)은 계약을 갖는다. 계약은 호출자(Client)가 이 연산의 소유자(Server)에게 메시지(Message)를 통해 연산을 요청할 때, 호출자의 입장에서 연산에 대해 알아야 하는 정보로써 요청 메시지, 응답 값, 연산 수행 후의 상태를 제약(Constraint)한다.

문제 상황

class A {
	public Response1 operation1(Message1 message1) {
    	Message2  message2 = ...;
    	Response2 response2 = B.operation2(message2);
        
        Response1 response1 = ...;
        return response1;
    }
}

class B {
	/**
    * 
    * @param Message2 message2 operation2의 메시지
    * @return operation2의 결과
    */
	public Response2 operation2(Message2 message2) {
    	Message3 message3 = ...;
        Response3 response3 = C.operation3(message3);
        
        Response2 response2 = ...;
        return response2;
    }
}

class C {
	/**
    * 
    * @param Message3 message3 operation3의 메시지
    * @return operation3의 결과
    * @throws Operation3Exception operation3의 예외
    */
	public Response3 operation3(Message3 message3) {
    	if (message3 = ...) {
        	throw new Operation3Exception(...);
        }
    	return ...;
    }
}

위와 같은 구조에서 A는 자신의 operation을 수행하기 위해 B의 operation을 호출하고, B 역시 C를 호출한다. A의 입장에서, B가 자신의 연산(operation2)를 수행하기 위해 C를 호출한다는 것을 알아야 하는가? 즉, A에게 B -> C의 계약(operation3)은 노출되어야 하는가?

원인 분석

위의 상황은 체결된 계약의 유형을 나누지 않았기 때문에 발생한다. DBC에 따르면 하나의 계약은 최대 4가지(응답과 예외 분리 시) 요소로 나뉠 수 있다.

계약 요소설명
메시지Client가 Server의 Operation을 사용하기 위해 Server로 전달하는 입력
응답Server가 Operation의 결과로 Client에게 전달하는 응답
불변식Server가 Operation 수행 후 보장하는 자신의 상태
예외응답의 한 종류로, Client에게 Operation 수행 실패를 전달

각 요소는 추이성에 따라 다른 계약으로 전파될 수 있는데, 추이성을 띄는 요소는 직전의 계약에 전파되어야 한다. 만약 계약에서 하나 이상의 요소가 추이성을 띈다면 그 계약은 추이적 계약으로 간주될 수 있고, 그것은 호출자가 명시적으로 처리하지 않는한, 호출자의 Operation에 대한 계약으로 전파되어야 한다.

추이성 유무

일반적으로 각 계약 요소의 추이성은 다음과 같다.

계약 요소추이성이유
메시지XOperation 내부에서 다른 Operation을 호출할 때 필요한 메시지는 현재 Operation의 호출자가 알 필요 없다.
응답XOperation 내부에서 다른 Operation을 호출한 결과는 현재 Operation의 호출자가 알 필요 없다.
불변식XOperation 내부에서 다른 Operation(B 소유)을 호출할 때 B의 불변식은 현재 Operation의 호출자가 알 필요 없다.

예외의 경우 계약의 실패를 의미하기 때문에 조금 더 세분화된다.

예외 종류추이성설명
Checked ExceptionO계약 실패 이유를 확인해야 하는 예외로, 명시적으로 처리하지 않는다면 추이성을 갖는다.
Unchecked ExceptionX계약 실패 이유를 확인할 수 없는 예외로, 추이성을 갖지 않는다.

Checked ExceptionUnchecked Exception은 다음을 기준으로 구분될 수 있다.

모든 하위 Operations가 정상일 때, Operation에서 발생 가능한가?

해결 방안

@Service
class UserService {
	private final UserRepository userRepository;
    
    /**
    * 
    * @param String userId desired id.
    * @return desired user.
    * @throws NotFoundException there is no user mapped by the id(CheckedException)
    */
    public User findById(String userId) {
    	Optional<User> user = userRepository.findById(userId);
        if (user.isEmpty()) {
        	throw new NotFoundException();
        }
        return user.get();
    }
}

@Repository
class UserRepository {
	private final SqlSession session;
    /*
    * @param String id desired id.
    * @return desired user.
    * @throws ClosedSessionException unexpected session error(Unchecked Exception)
    */
	public Optional<User> findById(String id) {
    	if (session.isClosed()) {
        	throw new ClosedSessionException();
        }
        return session.select(id);
    }
}

UserService.findById는 사용자를 찾기 위해 UserRepository.findById를 호출한다. 이때 Session이 의도치 않게 닫힌 경우, UserRepository의 계약은 실패하고 예외를 발생시킨다. 이때 발생된 ClosedSessionException은 Session이 정상 동작하는 경우 발생하지 않았을 예외로, Unchecked Exception에 해당한다.
반대로, UserService에서 사용자를 찾지 못한 경우 발생하는 NotFoundException은 UserRepository가 정상 동작하더라도 발생 가능하기 때문에 Checked Exception에 해당한다.
이에 따라 UserService.findById의 계약에는 ClosedSessionException이 전이되지 않고, 이는 문서로 명문화될 수 있다.
한발 더 나아가 UserService.findById의 호출자가 NotFoundException을 자신의 계약에 포함시키고 싶지 않다면, 그것은 명시적으로 처리되어야 한다.

@Service
class AlarmService {
	private final UserService userService;
    
    /**
    *
    * @param userId desried id.
    * @return whether the alarm is notified to the user or not.
    */
    public boolean notify(String userId) {
    	try {
	        final var user = userService.findById(userId);
        } catch (NotFoundException e) {
            return false;
        }
        ...
        return true;
    }
}       

AlarmService.notify는 명시적으로 NotFoundException을 처리하기 때문에 자신의 계약에 그것을 전이시키지 않는다.

결론

"Working software over comprehensive documentation" by Agile Manifesto

문서와 코드는 상호보완되어야 한다. "Don't reinvent the wheel"라는 격언과 Agile 원칙을 고려할 때, 문서는 코드의 세부사항을 하나하나 다루기보다, 어떤 계약이 체결되었는가?에 집중해야 한다.

PS. Java Checked, Unchecked Exception

Java는 언어 차원에서 명시적인 Checked Exception을 지원한다.

public User findById(String userId) throws NotFoundException {}

이는 때때로 유용하나, 모든 상황에서 throws로 명시하기란 어려운데, 예를 들어 Stream을 사용하는 경우, Function, Consumer 등의 IF에 Checked Exception이 포함되지 않기 때문에 부가적인 작업이 요구된다. 따라서 Checked, Unchecked 예외는 맥락적 차원과 언어적 차원을 구분하여 이해하는 것이 필요하다.

참고 자료

profile
夫唯嗇是以早服

0개의 댓글