트러블슈팅클린코드
4

암묵적 권한 체크에서 명시적 응답 설계로

실무에서 마주친 작은 설계 문제와 그걸 고쳐나간 과정을 기록합니다.

암묵적 권한 체크에서 명시적 응답 설계로

문제의 시작

경비사용 내역 화면을 개발하던 중, 관리자와 일반 사원을 구분해서 UI를 다르게 보여줘야 했다.

  • 관리자: 사원번호 검색 가능, 타인의 경비 내역 조회 가능
  • 일반 사원: 본인 정보 고정, 본인 경비 내역만 조회

권한 체크를 위해 백엔드 API를 하나 만들었는데, 처음엔 이렇게 짰다.

// 프론트
const isAdmin = async () => {
  const response = await http.post("/auth/checkAdmin", { empNo: empNo });
  return response.data.empNm; // 관리자면 이름 반환, 아니면 null
};

// 사용부
hasEditAuth.value = await isAdmin();
// hasEditAuth가 truthy(이름)면 관리자, falsy(null)면 일반 사원
-- 백엔드 쿼리
SELECT EMP_NM AS empNm
  FROM TB_USER A
 WHERE A.EMP_NO = #{empNo}
   AND EXISTS (
       SELECT 1
         FROM TB_USER_AUTH B
        WHERE B.EMP_NO = A.EMP_NO
          AND B.AUTH_CD = 'ADMIN'
   )

관리자가 아니면 쿼리 결과가 없으니까 이름도 못 가져오겠지 라는 논리였다.


뭐가 문제일까?

겉으로 보기엔 동작한다. 하지만 코드를 다시 보면 이상한 점이 보인다.

1. 함수 이름과 반환값이 불일치

const isAdmin = async () => {
  // 이름은 isAdmin → boolean을 반환해야 할 것 같은데
  return response.data.empnm; // string | null 반환
};

isAdmin()이라는 이름은 true/false를 기대하게 만든다.
하지만 실제로는 사원 이름(string) 을 반환하고 있다.

2. 암묵적인 규칙이 코드 어디에도 없다

"이름이 있으면 관리자, 없으면 일반 사원"

이 규칙은 코드 어디에도 명시되어 있지 않다.
새로운 팀원이 코드를 보면 절대 알 수 없다.

3. 백엔드 변경에 취약하다

백엔드에서 응답 구조를 조금만 바꿔도 권한 로직이 조용히 깨진다.
에러도 안 나고, 그냥 모든 사람이 관리자가 되거나 일반 사원이 되어버린다.


개선 방향

Step 1. 쿼리에서 명시적으로 isAdmin 반환

기존 쿼리는 EXISTSWHERE 필터로 사용했다.
이걸 SELECT 절의 CASE WHEN으로 옮겼다.

-- 개선 전: 관리자가 아니면 행 자체가 없음
SELECT EMP_NM AS empNm
  FROM TB_USER A
 WHERE A.EMP_NO = #{empNo}
   AND EXISTS (...)

-- 개선 후: 항상 1행 반환, isAdmin을 명시적으로 내려줌
SELECT EMP_NM AS empNm,
       CASE
           WHEN EXISTS (
               SELECT 1
                 FROM TB_USER_AUTH B
                WHERE B.EMP_NO = #{empNo}
                  AND B.AUTH_CD = 'ADMIN'
           ) THEN true
           ELSE false
       END AS isAdmin
  FROM TB_USER A
 WHERE A.EMP_NO = #{empNo}
항목개선 전개선 후
관리자 결과1행 (이름 있음)1행 (isAdmin: true)
비관리자 결과0행 (이름 없음)1행 (isAdmin: false)
규칙 명시 여부❌ 암묵적✅ 명시적

Step 2. 프론트 상태 관리 단순화

처음엔 권한 상태를 별도 ref로 관리했다.

// ❌ 두 곳에서 중복 관리
const hasEditAuth = ref({ isAdmin: false, empNm: '' });
const searchData = ref({ empNo: '', empNm: '', ... });

생각해보면 searchData 하나로 통합할 수 있다.

// ✅ searchData로 통합
const searchData = ref({
  compCd: '',
  spdngDtSt: '',
  spdngDtEd: '',
  empNo: '',
  empNm: '',
  isAdmin: false  // 권한도 여기서 관리
});

Step 3. isAdmin 함수 최종 정리

const isAdmin = async () => {
  const response = await http.post("/auth/checkAdmin", { empNo: empNo });

  searchData.value.isAdmin = response.data.isAdmin; // boolean
  searchData.value.empNm = response.data.empNm;

  // 비관리자면 본인 사원번호 고정 (authStore에서 가져옴)
  if (!searchData.value.isAdmin) {
    searchData.value.empNo = empNo;
  }
};

Step 4. 템플릿 v-if/v-else 정리

<!-- ❌ v-if + v-if(!): Vue가 독립적인 두 조건으로 평가 → 렌더링 충돌 가능 -->
<v-text-field v-if="searchData.isAdmin" ... />
<v-text-field v-if="!searchData.isAdmin" ... />

<!-- ✅ v-if/v-else: 하나의 분기로 인식 → 안전 -->
<v-text-field v-if="searchData.isAdmin" ... />
<v-text-field v-else ... />

정리

항목개선 전개선 후
권한 판단 기준이름 유무 (암묵적)isAdmin boolean (명시적)
함수 반환 타입string | null없음 (직접 세팅)
상태 관리hasEditAuth + searchDatasearchData 통합
템플릿 분기v-if + v-if(!)v-if + v-else

느낀 점

처음엔 "그냥 동작하면 되지 않나?" 싶었다.
하지만 코드를 다시 봤을 때 내가 짠 코드인데도 왜 이렇게 동작하는지 한 번에 이해가 안 됐다.

그게 문제였다.

좋은 코드는 동작하는 코드가 아니라, 읽었을 때 의도가 바로 보이는 코드라는 걸 다시 한번 느꼈다.


잘못된 내용이 있다면 댓글로 알려주세요!

댓글

(0)
암묵적 권한 체크에서 명시적 응답 설계로 | 강민석의 개발블로그