[Java] if문 (단축 평가 및 가독성 높이는 방법)

🌈 m1naworld ·2022년 11월 13일
1

자바를 잡아! 👻

목록 보기
16/19
post-thumbnail

우연히 호진님이 공유해주신 유튜브영상을 보고 if문에 대해 정말 얕게만 알고 응용하지는 못했구나를 깨달았다. 이를 계기로 if문을 좀 더 잘 활용할 수 있도록 알아보았으며, if문의 연산 순서 등을 고려하여 좋은 퍼포먼스를 기대할 수 있는 단축 평가의 개념가독성을 높이는 좋은 분기문을 쓰는 방법에 대해 알게 되었다!


단축 평가

단축 평가는 &&연산||연산의 원리를 이해하면 쉽다.

  1. if(condition1 && condition2 && condition3 && condition4)

    &&연산자로 여러 boolean값의 상황이 있을 때, &&연산은 하나의 상황이라도 false면 결과가 false이기 때문에 condition1이 false라면 굳이 뒤에 있는 condition2, condition3, condition4가 false인지 true인지 검사할 필요가 없다.

  1. if(condition1 || condition2 || condition3 || condition4)

    마찬가지로 위와 같이 ||연산자로 여러 boolean값의 상황이 있을 때는 하나의 상황이라도 true값이면 결과가 true이기 때문에 condition1이 true면 뒤의 condition2~4가 false인지 true인지 검사할 필요가 없다.


위와 같이 검사할 필요가 없다는 것을 컴파일러가 알고 뒤에 연산을 무시하는 것을 "단축평가"라고 한다.
이런 단축 평가라는 기능이 있기 때문에 이를 활용해서 성능을 높이는 작업을 하면 좋다.
예를 들면 true, false값이 간단하게 나오는 상황을 앞 쪽에다가 적용하면 이미 앞에서 boolean값이 결정되기 때문에 빠르게 처리할 수 있다.


❗️그러나, 단축평가가 마냥 좋은 것만은 아니니 주의하면서 사용해야 한다.

  1. 뒤의 연산들을 판단하지 않기 때문에 단위 테스트를 할 때 위험이 있다. 테스트 할 때 모든 조건을 테스트를 해줘야 하는데 앞에서 true, false를 결정해버려서 뒤에 조건을 정확하게 테스트 할 수 없기 때문이다.
  2. 단축 평가를 한다고 해서 무조건 빠른건 아니다! 어떠한 경우에는 오히려 연산의 순서를 정하기 때문에 병렬처리를 막아 성능 저하를 시킬 수도 있다고한다.

결과적으로 if문 퍼포먼스를 높이려면 괄호를 적절히 사용해 원하는 연산 순서로 진행되게 하면서, 간단한 조건을 먼저 연산할 수 있도록 하면 좋다.



가독성 높이는 if문 쓰는 법

if문을 작성할 때, 가장 주의할 점은 깊이(Deep)이다.
이중, 삼중, N 중 중첩문을 최대한 기피하여야 한다. 이는 코드 복잡도를 올릴 뿐만 아니라 코드에 대한 가독성도 무차별적으로 파괴시키는 안 좋은 방식이며 가능하다면 최대한 깊이가 1인 분기문을 작성하는게 좋다고 한다.

1. 보호절 숙어(Guard Clause)

Guard Clause이란, 사전 조건이 판별하여 거짓이라면 예외 혹은 반환 처리하여 더 이상 다음 단계가 실행되지 않도록 사전에 차단하는 방식이다.

if (!isEmpty) {
	// 메인로직
}

위의 코드를 Guard Clause로 작성하면 다음과 같다.

if (isEmpty) {
    return;
}
// 메인로직

이는 로직이 길어질 수록 더욱 효과적으로 눈에 띄게 가독성이 올라간다.

// 기존 중첩 if 문
void compute() {
    Server server = getServer();
    if (server != null) {
        Client client = server.getClient();
        if (client != null) {
            Request current = client.getRequest();
            if (current != null) {
                // 실제 처리할 로직
            }
        }
    }
}



/// 개선된 중첩 if문 - Guard Clause case
void compute() {
    Server server = getServer();
    if (server == null)
        return;
    Client client = server.getClient();
    if (client == null)
        return;
    Request current = client.getRequest();
    if (current == null)
        return;
    // 실제 처리할 로직    
}

2. if/else 블록의 순서

  • 가능하다면 if조건 안에는 긍정 조건을 넣어야 가독성이 좋음
  • 단, if/else 처리 로직 중 간단한 로직을 먼저 if절에 넣는 것이 가독성이 좋음
  • 단 보호절 숙어가 우선순위가 높음. if가 계층구조로 중첩될 거 같다면 부정의 조건을 넣어서 계층 구조가 사라지게 하는 것이 더욱 좋음
// 가능하면 긍정적인 조건 
if (...) {
    // 간단하고 긍정적인 내용
} else {
    //상대적으로 복잡한 내용
}

3. 드모르간 법칙

괄호 중첩이 적을수록 좋다.

// 기존 
if(!(hasfile && !isPrivate)){return false;}

// 개선
if( !hasfile || isPrivate) {return false;}

4. 복잡한 분기 추출

재사용되거나, 혹여 재사용되지 않더라도 분기절내의 복잡한 내용을 메소드로 표현하게 되면 더 나은 가독성을 얻을 수 있다.

// 기존
if(request.user.id==document.owner_id){
    //사용자가 이 문서를 수정할 수 있다.
}
if(request.user.id!=document.owner_id){
    //문서는 읽기 전용이다.
}
    
    
// 개선
final boolean isOwner = this.isOwner(request, document);

if(isOwner){
    //사용자가 이 문서를 수정할 수 있다.
}
if(!isOwner){
    //문서는 읽기 전용이다.
}

...
private boolean isOwner(Request request, Document document) {
    return request.user.id == document.owner_id;
}

5. loop 구문 내 분기처리

continue 이용하자!

while (line = reader.readline()) {
    if (line.startsWith('#') || line.isEmpty())
        continue;
    // 실제 처리할 로직
}

6. 삼항 연산자

간단한 구문일 경우 삼항연산자를 통해 코드를 한 라인으로 적절한 가독성을 확보할 수 있다. 만약 한 라인으로 처리하기 복잡할 경우 if/else를 쓰는 것이 훨씬 유리하다.

// 삼항 연산자가 유리한 경우 
String timeType = hour < 12 ? "am" : "pm";


// if/else가 유리한 경우 
String timeKor, timeEng;
if (hour < 12) {
    timeKor = "오전";
    timeEng = "am"
} else {
    timeKor = "오후";
    timeEng = "pm";
    callPmCustomAction();
}

7. 복잡한 논리 가독성 향상

한 라인으로 논리를 표현하지 않고, 가독성을 위해 적절한 여러 라인으로 분리하여 보호절과 유사하게 한다.

public class Range {
    private int bgn;
    private int end;
    ...
    
    // 기존
    // this의 bgn이나 end가 other의 bgn이나 end에 속하는지 확인
    private boolean isOverlapsWith( Range other ) {
      return ( this.bgn >= other.bgn && this.bgn < other.end )
          || ( this.end > other.bgn && this.end <= other.end )
          || ( this.bgn <= other.bgn && this.end >= other.end );
    } 
  
 
    // 개선
    // begin이나 end가 other에 속하는지 확인
    private void overlapsWith(Range other){
        // 우리가 시작하기 전에 끝난다.
        if (other.end <= begin) return false;
        // 우리가 끝난 후에 시작한다.
        if (other.begin >= end) return false;
        // 겹친다.
        return true;
    }    
}
 

8. 조건문 인수 순서

좌변에 유동적인 값이나 표현을 넣고, 우변엔 상수와 같은 고정값을 표현을 넣어 가독성을 좋게 한다. 그러나 이는 개발 팀마다 스타일이 다를 수 있으니 유의하자!

if (10 <= length)

// 아래가 더 낫다.
if (length >= 10)

*적용해보기

기존 프로젝트에서 회의실 예약 로직을 위해 조건문을 엄청나게 달았던 기억이 남아 코드를 가독성이 좋게 리팩토링 해보았다.

  • 기존 코드
@ApiOperation(value = "예약하기", notes = "보내는 데이터: 회의실아이디, 회의실타입, 시작시간, 종료시간, 팀원데이터")
@PostMapping(value = "/conference")
public ResponseEntity<Map<String, Object>> conferenceRoomBooking(@RequestBody PostBookingDataDto postBookingDataDto, HttpServletResponse response){

	UserDto user = authService.checkUser(response);
    Map<String, Object> map = new HashMap<>();
	Map<String, Object> arr = new HashMap<>();

	if (user != null) {
    	List<String> teamMateNames = postBookingDataDto.getTeamMate();

		String roomType;

		if (postBookingDataDto.getRoomType().equals("meeting")) {
        	roomType = "회의실";
        } else if (postBookingDataDto.getRoomType().equals("nabox")) {
        	roomType = "나박스";
        } else {
        	roomType = "스튜디오";
        }


		if (user.getClasses() != 0) {
        	// 신청자 회의실 하루에 한번만 예약 체크 or 나박스 최대 시간 예약 체크
            Map<Boolean, String> checkBooking = participantsService.checkBookingUser(postBookingDataDto.getRoomType(), user.getUserId());
            // 신청자가 예약이 안되어 있으면 or 나박스 최대 시간 안채웠으면
            if (checkBooking.containsKey(true)) {
            // 룸타입이 회의실일 경우인데 팀메이트가 비어있는 경우
            	if (roomType.equals("회의실") && teamMateNames.isEmpty()) {
           			arr.put("fail", "회의실은 2인이상일 경우만 예약하실 수 있습니다️.");
                    map.put("message", arr);
                    return ResponseEntity.status(HttpStatus.OK).body(map);
                } else {
                	// 동시간대 회의실 사용자
                    Set<String> usingUsers = participantsService.checkUsingBooking(user, postBookingDataDto);
                    if (!usingUsers.isEmpty() && (roomType.equals("회의실"))) {
                    	arr.put("fail", "현재 동시간대 예약중인" + usingUsers + "님이 포함되어 있습니다.");
                        map.put("message", arr);
                        return ResponseEntity.status(HttpStatus.OK).body(map);
                    } else if (!usingUsers.isEmpty()) { //나박스일 때
                    	arr.put("fail", "동시간대 다른 예약은 불가합니다.");
                        map.put("message", arr);
                        return ResponseEntity.status(HttpStatus.OK).body(map);
                    } else {
                    	// 신청하는 회의실 예약 확인
                        boolean usingRoom = bookingService.findSameBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime());
                        if (usingRoom) {
                        	arr.put("fail", "❌ 이미 예약이 완료된 회의실 입니다.");
                            map.put("message", arr);
                            return ResponseEntity.status(HttpStatus.OK).body(map);
                        } else {
                        	// 예약 O
                            // 회의실 일때 // 스튜디오일때는
                            if (roomType.equals("회의실") || roomType.equals("스튜디오")) {
                            	int bookingId = bookingService.insertBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime(), false);
                               
                               if (roomType.equals("회의실")) {
                                   // 유저들 이름 종합
                                   List<UserDto> users = userService.getUsersData(user.getClasses(), user.getUserName(), teamMateNames);
                                   participantsService.insertParticipants(bookingId, users, teamMateNames);
                                   arr.put("success", "회의실 예약 성공! ♥ ");
                                } else {
                                	participantsService.insertApplicant(bookingId, user.getUserId());
                                    arr.put("success", "스튜디오 예약 성공! 비밀번호는 매니저님께 문의해주세요! ♥");
                                }
                                map.put("message", arr);
                                return ResponseEntity.status(HttpStatus.OK).body(map);
                                } else { // 나박스일때
                                    boolean timeResult = participantsService.checkUsingTime(postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime());
                                    if (checkBooking.containsValue("add") && !timeResult) {
                                    arr.put("fail", "나박스 하루 최대 이용시간은 2시간 입니다.");
                                	} else {
                                    	int bookingId = bookingService.insertBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime(), false);
                                        participantsService.insertApplicant(bookingId, user.getUserId());
                                        arr.put("success", roomType + " 예약 성공! ♥ ");

                                    }
                                    map.put("message", arr);
                                    return ResponseEntity.status(HttpStatus.OK).body(map);
                               }
                           }
                       }
                   }
               } else {
                    if (roomType.equals("나박스")) {
                        arr.put("fail", roomType + "예약은 하루 최대 두시간만 가능합니다.");
                    } else {
                        arr.put("fail", roomType + "예약은 하루에 한번만 가능합니다.");
                    }
                    map.put("message", arr);
                    return ResponseEntity.status(HttpStatus.OK).body(map);
                }
        } else { // 매니저님들 공식일정 등록
            // 4층이 아닐경우 공식일정
            if (postBookingDataDto.getRoomId() / 100 != 4) {
                // 기존 같은 회의실, 같은 시간대 공식일정 확인
                List<Boolean> checkOfficial = bookingService.checkOfficial(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime());
                    // 있을 경우
                if (!checkOfficial.isEmpty()) {
                	arr.put("fail", "이미 공식일정이 등록되어있습니다.");
                } else { // 없을 경우
                    // 기존 인재들 예약 cancel상태로 변경 및 공식 일정 예약
                    int resultBookingId = bookingService.updateBooking(postBookingDataDto.getRoomId(), user.getUserId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime(), true);
                    // 누가 예약하고 쓰는지 참석자 등록
                    List<UserDto> users = userService.getUsersData(user.getClasses(), user.getUserName(), teamMateNames);
                    System.out.println(users);
                    participantsService.insertParticipants(resultBookingId, users, postBookingDataDto.getTeamMate());

					arr.put("success", roomType + " 공식 일정 등록 완료 ✅");
                }
            } else { // 4층일 경우
            	boolean usingRoom = bookingService.findSameBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime());
                if (usingRoom) {
                	arr.put("fail", "이미 예약이 완료된 회의실 입니다.");
                } else {
                	int bookingId = bookingService.insertBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime(), false);
                       
                    // 유저들 이름 종합
                    List<UserDto> users = userService.getUsersData(user.getClasses(), user.getUserName(), teamMateNames);
                    participantsService.insertParticipants(bookingId, users, teamMateNames);
                    arr.put("success", roomType + "예약 성공! ♥ ");
                }
            }
            map.put("message", arr);
            return ResponseEntity.status(HttpStatus.OK).body(map);
        }
    } else {
    	map.put("message", "tokenFail");
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(map);
    }
}

  • 개선한 코드
@ApiOperation(value = "예약하기", notes = "보내는 데이터: 회의실아이디, 회의실타입, 시작시간, 종료시간, 팀원데이터")
@PostMapping(value = "/conference")
public ResponseEntity<Map<String, Object>> conferenceRoomBooking(@RequestBody PostBookingDataDto postBookingDataDto, HttpServletResponse response) {

	UserDto user = authService.checkUser(response);
    Map<String, Object> map = new HashMap<>();
    Map<String, Object> arr = new HashMap<>();

	if (user == null) {
    	map.put("message", "tokenFail");
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(map);
    } // 토큰이 유효한 유저인 경우

	// 인재인 경우
    if(user.getClasses() != 0) {
    	String roomTypeUpper = postBookingDataDto.getRoomType().toUpperCase();
        RoomType roomType = RoomType.valueOf(roomTypeUpper);
        System.out.println(roomType);

		// 회의실, 스튜디오 예약 횟수 초과했는지 나박스 예약 최대시간 초과했는지 확인
    	Map<Boolean, String> checkBooking = participantsService.checkBookingUser(roomType.lowerCase, user.getUserId());
    	// 예약 횟수 및 예약 최대시간 꽉 찬 경우
    	if (checkBooking.containsKey(false)) {
    		if (roomType.lowerCase.equals(RoomType.NABOX.lowerCase)) {
        		arr.put("fail", roomType.name + "예약은 하루 최대 두시간만 가능합니다.");
        	} else {
        		arr.put("fail", roomType.name + "예약은 하루에 한번만 가능합니다.");
        	}
        	map.put("message", arr);
        	return ResponseEntity.status(HttpStatus.OK).body(map);
    	}

    	// 동시간대 예약자 있는지 확인
    	Set<String> usingUsers = participantsService.checkUsingBooking(user, postBookingDataDto);
    	// 동시간대 예약자가 있는 경우
    	if (!usingUsers.isEmpty()) {
    		if (roomType.lowerCase.equals(RoomType.MEETING.lowerCase)) {
        	arr.put("fail", "현재 동시간대 예약중인" + usingUsers + "님이 포함되어 있습니다.");
        	} else {
        		arr.put("fail", "동시간대 다른 예약은 불가합니다.");
        	}
        	map.put("message", arr);
        	return ResponseEntity.status(HttpStatus.OK).body(map);
    	}

		// 예약된 회의실인지 확인
    	boolean usingRoom = bookingService.findSameBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime());
    	// 이미 예약된 회의실일 경우
    	if (usingRoom) {
    		arr.put("fail", "❌ 이미 예약이 완료되어 예약이 불가합니다.");
        	map.put("message", arr);
        	return ResponseEntity.status(HttpStatus.OK).body(map);
    	}


		// 나박스가 아닌 회의실, 스튜디오 예약 로직
    	if (!roomType.lowerCase.equals(RoomType.NABOX.lowerCase)) {
    	int bookingId = bookingService.insertBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime(), false);
        	// 회의실인 경우
        	if (roomType.lowerCase.equals(RoomType.MEETING.lowerCase)) {
        		List<UserDto> users = userService.getUsersData(user.getClasses(), user.getUserName(), postBookingDataDto.getTeamMate());
            	participantsService.insertParticipants(bookingId, users, postBookingDataDto.getTeamMate());
            	arr.put("success", "회의실 예약 성공! ♥ ");
        	} else { 
        		// 스튜디오인 경우
            	participantsService.insertApplicant(bookingId, user.getUserId());
            	arr.put("success", "스튜디오 예약 성공! 비밀번호는 매니저님께 문의해주세요! ♥");
        	}
        	map.put("message", arr);
        	return ResponseEntity.status(HttpStatus.OK).body(map);
    	}

		// 나박스 예약 로직
    	boolean timeResult = participantsService.checkUsingTime(postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime());
    	if (checkBooking.containsValue("add") && !timeResult) {
        	arr.put("fail", "나박스 하루 최대 이용시간은  입니다.");
        } else {
        	int bookingId = bookingService.insertBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime(), false);
            participantsService.insertApplicant(bookingId, user.getUserId());
            arr.put("success", roomType + " 예약 성공! ♥ ");
        }
        map.put("message", arr);
        return ResponseEntity.status(HttpStatus.OK).body(map);

    } // 매니저인 경우

	// 4층이 아닐경우 공식일정
    if (postBookingDataDto.getRoomId() / 100 != 4) {
    	// 공식일정 예약 확인
        List<Boolean> checkOfficial = bookingService.checkOfficial(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime());

		// 이미 공식 예약인 경우
        if (!checkOfficial.isEmpty()) {
        	arr.put("fail", "이미 공식일정이 등록되어있습니다.");
        } else { // 없을 경우
        	// 기존 인재들 예약 cancel상태로 변경 및 공식 일정 예약
            int resultBookingId = bookingService.updateBooking(postBookingDataDto.getRoomId(), user.getUserId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime(), true);
            // 예약자 등록
            List<UserDto> users = userService.getUsersData(user.getClasses(), user.getUserName(), postBookingDataDto.getTeamMate());
            participantsService.insertParticipants(resultBookingId, users, postBookingDataDto.getTeamMate());
            arr.put("success", "공식 일정 등록 완료 ✅");
        }
        map.put("message", arr);
        return ResponseEntity.status(HttpStatus.OK).body(map);
    } // 4층일 경우

   	// 회의실이 예약되었는지 확인
    boolean usingRoom = bookingService.findSameBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime());
    if (usingRoom) {
    	arr.put("fail", "이미 예약이 완료된 회의실 입니다.");
    } else {
    	int bookingId = bookingService.insertBooking(postBookingDataDto.getRoomId(), postBookingDataDto.getStartTime(), postBookingDataDto.getEndTime(), false);
        // 유저들 이름 종합
        List<UserDto> users = userService.getUsersData(user.getClasses(), user.getUserName(), postBookingDataDto.getTeamMate());
        participantsService.insertParticipants(bookingId, users, postBookingDataDto.getTeamMate());
        arr.put("success", " 회의실 예약 성공! ♥ ");
    }
    map.put("message", arr);
    return ResponseEntity.status(HttpStatus.OK).body(map);
}

아직도 복잡하지만, 훨씬 보기 편해졌다! 확실히 아는 것이 많아질 수록 코드를 짤 때 그냥 마냥 짜는 것이 아닌 위와 같은 것들을 고려하면서 짜게 된다. 더욱 좋은 코드를 작성할 수 있는 나를 바란다.👏🏻



Ref.
기본기를 쌓는 정아마추어 코딩 블로그
DevOOOOOOOOP

profile
개발자로 사는 내 삶은 즐거워 👾

0개의 댓글