트러블슈팅›클린코드•
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 반환
기존 쿼리는 EXISTS를 WHERE 필터로 사용했다.
이걸 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 + searchData | searchData 통합 |
| 템플릿 분기 | v-if + v-if(!) | v-if + v-else |
느낀 점
처음엔 "그냥 동작하면 되지 않나?" 싶었다.
하지만 코드를 다시 봤을 때 내가 짠 코드인데도 왜 이렇게 동작하는지 한 번에 이해가 안 됐다.
그게 문제였다.
좋은 코드는 동작하는 코드가 아니라, 읽었을 때 의도가 바로 보이는 코드라는 걸 다시 한번 느꼈다.
잘못된 내용이 있다면 댓글로 알려주세요!