5. 이슈는 상시 있는 것 (hard coding)

2025. 4. 9. 18:38Legacy

반응형

개발자 G의 업무는 더더욱 바빠졌다. 푸시 메시지가 가기 시작한 이후로 관련된 업무는 모두 G에게 할당되었고, 그 밖에 처리해야 할 이슈들도 사안에 따라 G에게 들어왔다. 자신에게 업무가 할당된다는 사실에 G는 기뻣지만, 업무 내용은 그렇지 못했다.

1. 통계 쿼리

통계 페이지에 들어가는 필드를 하나 추가해달라는 요청을 받았다. 시스템에는 특수한 통계 페이지가 각 담당자마다 존재하고 있고, 그 쿼리들은 공통적으로 수많은 필드들을 불러오고 있었다.

<select id="getCueList" resultType="java.util.Map">
	SELECT A, B, C, ... 
	FROM CUE_TABLE
    ...
</select>

MyBatis 를 활용하고 있기 때문에 디버깅이 쉽지 않은 점도 있고, 위의 간단해보이는 쿼리는 많은 문제점을 가지고 있다.

Map 은 Key - Value 형태라면 어떤 것이든 데이터로 제공받을 수 있게 해주는 장점이 많은 객체이다. 하지만 그 장점은 객체지향 프로그래밍에서 과도하게 사용하게 되면 오히려 단점이 된다. 현재 판매중인 큐의 현황을 보여주는 위 쿼리를 엔드포인트로 구현하려면 아래와 같은 코드를 작성하게 된다.

@Mapper
public interface CueMapper {
	Map getCueList(Map requestData);
}

@Service
public class CueStatService {
	@Autowired
    private CueMapper cueMapper;
    
    public Map getCueList(Map requestData) {
    	return cueMapper.getCueList(requestData);
    }
}

@RestController
public class CueStatController {
	@Autowired
    private CueStatService cueStatService;
    
    @GetMapping("/cues")
    public Map getCueList() {
    	Map requestData = new HashMap();
        requestData.put(...);
        
    	return cueStatService.getCueList(requestData);
    }
}

만약 이 서비스에 조회해야 할 필드 D를 하나 추가해야 한다면? 엔드포인트까지의 비즈니스 로직에는 아무런 변화를 주지 않아도 된다.

<select id="getCueList" resultType="java.util.Map">
	SELECT A, B, C, D... 
	FROM CUE_TABLE
    ...
</select>

그러 쿼리에 D라는 Column을 하나 더 조회하는 것으로 추가하면(필요하면 DB에 추가)모든 반영이 끝난다.

이러면 수정 반영에는 장점을 갖지만, 유지보수성에서 최악의 결과를 낳는다. 개발자는 기능을 수정해야 할 경우 쿼리를 찾기 위해 위 코드를 역순으로 추적해야 하고, 정확히 어떤 필드들이 포함되는 지 코드를 보고 알 수 없기 때문에 쿼리 콘솔에 직접 쿼리를 실행해야 한다. 심지어 Alias 도 없기 때문에 필드명에 Column을 그대로 노출한다.

G는 resultType 을 해당하는 VO 객체로 전환했다.

2. 트랜잭션

큐 판매글 작성시 사진이 함께 올라가지 않는다는 이슈가 들어왔다.

@Service
public class CueService {
	...
    
	public void uploadCueSellingPost(Map request) {
    	uploadPostInfo(request);
        uploadPhoto(request);
    }
    
    ...
}

insert query 를 두개 실행하는데, @Transactional 을 사용하지 않고 있다.

@Service
public class CueService {
	...
    
    @Transactional
	public void uploadCueSellingPost(Map request) {
    	uploadPostInfo(request);
        uploadPhoto(request);
    }
    
    ...
}

어노테이션만 사용해 줘도 파괴적인 로직 실패를 막을 수 있다.

3. 관심사 분리

특정 큐가 등록 금지 매물인 경우에는 매물 리스트에서 제외해달라는 요구사항이 있었다. 기존 구현은 가히 충격적이다.

<c:if data="${not (que_id eq 'que1234' || que_id eq 'que1235' || ...)}">
	<script>
    	...
    </script>
</c:if>

등록 금지 매물이 1개 추가될 때마다 클라이언트에서 분기에 매물 코드를 하나씩 추가하고 있다. 이렇게 하면 너무나도 많은 문제점이 노출된다.

  • 문제가 되는 큐 매물이 왜 등록 금지 매물인지 코드를 보고 알 수 없다.
  • 등록 금지 사유가 다르더라도 큐의 코드만 기록하면 소스를 통과하게 된다.
  • API 호출 결과는 모든 큐 매물이 담겨있지만 클라이언트에서 필터링하고 있으므로, 스크립트를 클라이언트에서 임의로 변조할 수 있다.

등등의 많은 이유를 들 수 있지만, 가장 큰 문제점은 '관심사가 분리되어 있지 않다는 것' 이다. 서버는 응답을 보낼 때까지 어떤 비즈니스적 이유가 있는지 모르고, 클라이언트에서 모든 데이터를 가져온 후에 판단해서 화면에 노출하는 것은 서버에게도 클라이언트에게도 리소스 낭비이다.

클라이언트 로직을 삭제하고, 서버에서 어떤 정책을 가지고 있는지 분석해서 소스로 녹여냈다.

반복되는 이슈

많은 이슈가 위 케이스와 궤를 같이한다. 한 줄 수정하면 요청자의 요구사항을 구현할 수 있지만, 시스템 요구사항에서는 점점 멀어진다. 기존 소스 작성자들은 요구사항이 생길 때마다 분기 하나, 코드 하나를 추가하는 방식으로 일했다. G는 이유도 모른채 추가되어 있는 가독성 없는 코드를 보고 있자니 머리가 어지러워졌다. 그리고 개선의 꿈을 꾸기 시작했다.

반응형