자바 제어문

Jeongmin Yeo (Ethan)·2020년 12월 11일
3

STUDY HALLE

목록 보기
4/13
post-thumbnail

자바가 제공하는 제어문을 정리합니다

백기선님과 함께하는 자바 4주차 스터디 과정입니다.

학습할 내용은 다음과 같습니다.

  • 선택문
  • 반복문
  • JUnit5

1. 선택문

제어문(control flow statements)

자바 프로그램이 원하는 결과를 얻기 위해서는 프로그램의 순차적인 흐름을 제어해야만 할 경우가 생깁니다.

이때 사용하는 명령문을 제어문이라고 하며, 이러한 제어문에는 조건문, 반복문 등이 있습니다.

이러한 제어문에 속하는 명령문들은 중괄호({})로 둘러싸여 있으며, 이러한 중괄호 영역을 블록(block)이라고 합니다.


조건문(conditional statements)

조건문은 주어진 조건식의 결과에 따라 별도의 명령을 수행하도록 제어하는 명령문입니다.

조건문 중에서도 가장 기본이 되는 명령문은 바로 if 문입니다.

자바에서 사용하는 대표적인 조건문의 형태는 다음과 같습니다.

  1. if 문
  2. if / else 문
  3. if / else if / else 문
  4. switch 문

if 문

// 문법

if (조건식) {

    조건식의 결과가 참일 때 실행하고자 하는 명령문;

}

//  if 문을 사용하여, 해당 문자가 영문 소문자인지를 확인하는 예제입니다.

if (ch >= 'a' && ch <= 'z') {

    System.out.println("해당 문자는 영문 소문자입니다.");

}

if / else 문

if 문과 함께 사용하는 else 문은 if 문과는 반대로 주어진 조건식의 결과가 거짓(false)이면 주어진 명령문을 실행합니다.

// 문법

if (조건식) {

    조건식의 결과가 참일 때 실행하고자 하는 명령문;

} else {

    조건식의 결과가 거짓일 때 실행하고자 하는 명령문;

}

//  if / else 문을 사용하여, 해당 문자가 영문 소문자인지 아닌지를 확인하는 예제입니다.

if (ch >= 'a' && ch <= 'z') {

    System.out.println("해당 문자는 영문 소문자입니다.");

} else {

    System.out.println("해당 문자는 영문 소문자가 아닙니다.");

}


switch 문

switch 문은 if / else 문과 마찬가지로 주어진 조건 값의 결과에 따라 프로그램이 다른 명령을 수행하도록 하는 조건문입니다.

이러한 switch 문은 if / else 문보다 가독성이 더 좋으며, 컴파일러가 최적화를 쉽게 할 수 있어 속도 또한 빠른 편입니다.

하지만 switch 문의 조건 값으로는 int형으로 승격할 수 있는(integer promotion) 값만이 사용될 수 있습니다.

즉, 자바에서는 swich 문의 조건 값으로 byte형, short형, char형, int형의 변수나 리터럴을 사용할 수 있습니다.

또한, 이러한 기본 타입에 해당하는 데이터를 객체로 포장해 주는 래퍼 클래스(Wrapper class) 중에서 위에 해당하는 Byte, Short, Character, Integer 클래스의 객체도 사용할 수 있습니다.

그리고 enum 키워드를 사용한 열거체(enumeration type)와 String 클래스의 객체도 사용할 수 있습니다.

// 문법 

switch (조건 값) {

    case1:

        조건 값이 값1일 때 실행하고자 하는 명령문;

        break;

    case2:

        조건 값이 값2일 때 실행하고자 하는 명령문;

        break;

    ...

    default:

        조건 값이 어떠한 case 절에도 해당하지 않을 때 실행하고자 하는 명령문;

        break;

}

// switch 문을 사용하여, 해당 문자가 영문자 모음인지를 확인하는 예제입니다. 

switch (ch) {

    case 'a':

        System.out.println("해당 문자는 'A'입니다.");

        break;

    case 'e':

        System.out.println("해당 문자는 'E'입니다.");

        break;

    case 'i':

        System.out.println("해당 문자는 'I'입니다.");

        break;

    case 'o':

        System.out.println("해당 문자는 'O'입니다.");

        break;

    case 'u':

        System.out.println("해당 문자는 'U'입니다.");

        break;

    default:

        System.out.println("해당 문자는 모음이 아닙니다.");

        break;

}

2. 반복문

반복문이란 프로그램 내에서 똑같은 명령을 일정 횟수만큼 반복하여 수행하도록 제어하는 명령문입니다.

프로그램이 처리하는 대부분의 코드는 반복적인 형태가 많으므로, 가장 많이 사용되는 제어문 중 하나입니다.

자바에서 사용되는 대표적인 반복문의 형태는 다음과 같습니다.

  1. while 문

  2. do / while 문

  3. for 문

  4. Enhanced for 문


while 문

while 문은 특정 조건을 만족할 때까지 계속해서 주어진 명령문을 반복 실행합니다.

while 문은 우선 조건식이 참(true)인지를 판단하여, 참이면 내부의 명령문을 실행합니다.

내부의 명령문을 전부 실행하고 나면, 다시 조건식으로 돌아와 또 한 번 참인지를 판단하게 됩니다.

이렇게 조건식의 검사를 통해 반복해서 실행되는 반복문을 루프(loop)라고 합니다.

자바에서 while 문의 문법은 다음과 같습니다.

// 문법

while (조건식) {

    조건식의 결과가 참인 동안 반복적으로 실행하고자 하는 명령문;

}

// while 문을 5번 반복해서 실행하는 예제입니다.

int i = 0;

while (i < 5) {

    System.out.println("while 문이 " + (i + 1) + "번째 반복 실행중입니다.");

    i++; // 이 부분을 삭제하면 무한 루프에 빠지게 됨.

}

System.out.println("while 문이 종료된 후 변수 i의 값은 " + i + "입니다.");

while 문 내부에 조건식의 결과를 변경하는 명령문이 존재하지 않을 때는 프로그램이 영원히 반복되게 됩니다.

이것을 무한 루프(infinite loop)에 빠졌다고 하며, 무한 루프에 빠진 프로그램은 영원히 종료되지 않습니다.

무한 루프는 특별히 의도한 경우가 아니라면 반드시 피해야 하는 상황입니다.

따라서 while 문을 작성할 때는 조건식의 결과가 어느 순간 거짓(false)을 갖도록 조건식의 결과를 변경하는 명령문을 반드시 포함시켜야 합니다.

위의 예제에서 조건식의 결과를 변경하는 명령문인 i++를 제거하면, 변수 i의 값은 언제나 1이기 때문에 무한 루프에 빠지게 됩니다.


do / while 문

while 문은 루프에 진입하기 전에 먼저 조건식부터 검사합니다.

하지만 do / while 문은 먼저 루프를 한 번 실행한 후에 조건식을 검사합니다.

즉, do / while 문은 조건식의 결과와 상관없이 무조건 한 번은 루프를 실행합니다.

자바에서 do / while 문의 문법은 다음과 같습니다.

// 문법

do {

    조건식의 결과가 참인 동안 반복적으로 실행하고자 하는 명령문;

} while (조건식);

// do / while 문은 조건식의 결과와 상관없이 한번 실행시켜주는 예제입니다.

int i = 1, j = 1;

while (i < 1) {

    System.out.println("while 문이 " + i + "번째 반복 실행중입니다.");

    i++; // 이 부분을 삭제하면 무한 루프에 빠지게 됨.

}

System.out.println("while 문이 종료된 후 변수 i의 값은 " + i + "입니다.");
 
do {

    System.out.println("do / while 문이 " + i + "번째 반복 실행중입니다.");

    j++; // 이 부분을 삭제하면 무한 루프에 빠지게 됨.

} while (j < 1);

System.out.println("do / while 문이 종료된 후 변수 j의 값은 " + j + "입니다.");

만약 while 문이었다면 단 한 번의 출력도 없었을 것입니다.


for 문

for 문은 while 문과는 달리 자체적으로 초기식, 조건식, 증감식을 모두 포함하고 있는 반복문입니다.

따라서 while 문보다는 좀 더 간결하게 반복문을 표현할 수 있습니다.

자바에서 for 문의 문법은 다음과 같습니다.

for (초기식; 조건식; 증감식) {

    조건식의 결과가 참인 동안 반복적으로 실행하고자 하는 명령문;

}

// 에제입니다.
for (i = 0; i < 5; i++) {

    System.out.println("for 문이 " + (i + 1) + "번째 반복 실행중입니다.");

}

System.out.println("for 문이 종료된 후 변수 i의 값은 " + i + "입니다.");

Enhanced for 문

JDK 1.5부터 Enhanced for 문이라는 반복문이 추가되었습니다.

이 반복문은 컬렉션 프레임워크와 배열에서 유용하게 자주 사용됩니다.

// 바로 예제부터 보겠습니다. 

int[] arr = new int[]{1, 2, 3, 4, 5};

for (int e : arr) {

    System.out.print(e + " ");

}

// 이렇게도 가능합니다. 

for (int i = 0; i < arr1.length; i++) {

  arr1[i] += 10; // 각 배열 요소에 10을 더함.

}


3. JUnit 5

기존의 JUnit 버전들과는 달리, JUnit 5는 서로 다른 하위 프로젝트로부터 기원한 서로 다른 모듈의 집합입니다.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform

    • 테스트를 발견하고 테스트 계획을 생성하는 TestEngine 인터페이스를 가지고 있습니다. Platform은 TestEngine을 통해서 테스트를 발경하고 ,실행하고 ,결과를 보고합니다.
  • JUnit Jupiter

    • TestEngine의 실제 구현체는 별도 모듈입니다. 모듈 중 하나가 jupiter-engine입니다. 이 모듈은 jupiter-api를 사용해서 작성한 테스트 코드를 발견하고 실행합니다. Jupiter API는 JUnit 5에 새롭게 추가된 테스트 코드용 API로서, 개발자는 Jupiter API를 사용해서 테스트 코드를 작성할 수 있습니다
  • JUnit Vintage

    • 기존에 JUnit 4 버전으로 작성한 테스트 코드를 실행할 때에는 vintage-engine 모듈을 사용합니다.

JUnit 5는 런타임시에 Java 8이상이 필요합니다.

스프링 부트 프로젝트를 만든다면 기본으로 JUnit 5 의존성이 추가됩니다.

스프링 부트 2.2 부터는 spring-boot-starter-test에 JUnit 5가 포함되어 있기 떄문입니다.


JUnit 5. Annotations

  • @Test

    • Test Method임을 나타내줍니다.
  • @RepeatedTest

    • 반복적인 테스트를 위한 테스트 템플릿을 말합니다.
  • @BeforeAll

    • 이 에노테이션은 모든 @Test, @RepeatedTest 전에 실행되어야 함을 나타냅니다.
  • @BeforeEach

    • 이 에노테이션은 각각의 @Test, @RepeatedTest 실행 전에 호출되는 에노테이션을 뜻합니다.
  • @AfterAll

    • 모든 @Test, @Repeated 에노테이션이 붙은 테스트가 실행된 후에 호출되는 에노테이션을 뜻합니다.
  • @AfterEach

    • 이 에노테이션은 각각의 @Test, @RepeatedTest 실행된 후에 호출되는 에노테이션을 뜻합니다.
  • @Disabled

    • 테스트를 비활성화 하는데 사용되는 에노테이션 입니다.
  • @DisplayName

    • 테스트 에노테이션을 설명하는 주석을 만들 수 있는 에노테이션입니다.
  • @DisplayNameGeneration

    • 모든 테스트 메소드에 커스텀한 이름 규칙을 지정할 때 사용되는 에노테이션 입니다.

JUnit 5. Assertion

JUnit 5에서는 Jupiter API assertion을 이용해 테스트를 할 수 있습니다.

자주 사용하는 Assertion은 다음과 같습니다.

  • assertEqulas(expected, actual)

    • 실제 값이 기대한 값과 같은지 확인합니다.
  • assertNotNull(actual)

    • 값이 null이 아닌지 확인합니다.
  • assertTrue(boolean)

    • 다음 조건이 참(true)인지 확인합니다.
  • assertAll(executables...)

    • 모든 확인 구문 확인합니다.
  • assertThrows(expectedType, executable)

    • 예외의 발생을 확인합니다.
  • assertTimeout(duration, executable)

    • 특정 시간 안에 실행이 완료되는지 확인합니다.

예제를 통해 보겠습니다.

@Test
void create_study(){
        Study study = new Study(10);
        
        assertNotNull(study);

        // assertEquals의 경우 첫번째인자가 기대하는 값, 나와야하는 값을 말하고 두번째 인자가 실제 객체의 값을 말한다. 세번째 인자가 실패했을 때 출력하는 메지
        assertEquals(StudyStatus.DRAFT, study.getStatus(), "스터디를 처음 만들면 상태가 DRAFT여야 한다.");

        assertTrue(study.getLimit() > 0 , "스터디 참여 제한 인원은 한명 이상이어야 합니다");

        assertAll(
                () -> assertNotNull(study),
                () -> assertEquals(StudyStatus.DRAFT, study.getStatus(), "스터디를 처음 만들면 상태가 DRAFT여야 한다."),
                () -> assertTrue(study.getLimit() > 0 , "스터디 참여 제한 인원은 한명 이상이어야 합니다")
        );

        assertThrows(IllegalArgumentException.class, () -> new Study(-10));

        IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, () -> new Study(-10));

        String message = illegalArgumentException.getMessage();

        assertEquals("limit는 0보다 커야한다.", message, "limit Error Message 비교 실패");

        assertTimeout(Duration.ofSeconds(10), () -> new Study(10));

        assertTimeout(Duration.ofMillis(100), () -> {
            new Study(10);
            Thread.sleep(90);
        });
    }

JUnit 5. 조건에 따라 테스트 실행하기

특정한 조건을 만족하는 경우에만 테스트를 실행하는 방법입니다.

실패할 경우 다음 코드를 실행하지 않습니다.

이를 위한 방법으로는 Assumptions @Enabled__ @Disabled___이 있습니다.

@Test
@EnabledOnOs(OS.MAC)
@DisabledOnOs(OS.WINDOWS)
@EnabledOnJre(JRE.JAVA_11)
void test2() {
	String test_env = System.getenv("TEST_ENV");

	System.out.println(test_env);

	assumeTrue("LOCAL".equalsIgnoreCase(test_env));

	assumingThat("LOCAL".equalsIgnoreCase(test_env), () -> {
    		System.out.println("AssumingThat");
    	});

	System.out.println("AssumeTrue");
 }

JUnit 5. 태깅과 필터링

테스트 그룹을 만들고 원하는 테스트 그룹만 테스트를 실행할 수 있는 기능입니다.

Intellij IDE를 기준으로 Edit Configuration에서 설정할 수 있습니다.

  • @Tag

    • 테스트 메소드에 태그를 추가할 수 있습니다.

    • 하나의 테스트 메소드에 여러 태그를 사용할 수 있습니다.

그리고 필요하다면 JUnit 5 애노테이션을 조합하여 커스텀 태그를 만들 수도 있습니다.

예시는 다음과 같습니다.

@Test
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Tag("FastTest")
public @interface FastTest {

}

JUnit 5. 테스트 반복하기

특정 에노테이션을 사용해 테스트를 반복하는 작업을 할 수 있습니다.

  • @RepeatedTest

    • 반복 횟수와 반복 테스트 이름을 설정할 수 있습니다.

    • 반복 정보를 담고 있는 RepetitionInfo를 매개변수로 받을 수 있습니다.

    • 특수 변수로 {displayName} , {currentRepetition}, {totalRepetitions}를 사용할 수 있습니다.

      • displayName: 테스트를 설명하는 주석
      • currentRepetition: 현재 반복 횟수
      • totalRepetitions: 총 반복 횟수
  • @ParameterizedTest

    • 테스트에 여러 다른 매개변수를 대입해가며 반복 실행을 할 수 있습니다.

    • 특수 변수로 {displayName}, {index}, {arguments}를 사용할 수 있습니다.

      • displayName: 테스트를 설명하는 주석
      • index: 현재 반복 횟수
      • arguments: 매개변수 리스트들
    • 인자 값들의 소스를 받을 수 있습니다.

      • @ValueSource

      • @NullSource, @EmptySource, @NullAndEmptySource

      • @EnumSource

      • @MethodSource

      • @CvsSource

      • @CvsFileSource

      • @ArgumentSource

    • 받는 인자 값을 통해서 내가 원하는 객체로 변환 할 수 있습니다.

    • 여러가지 인자 값을 조합해서 내가 원하는 객체로 변환 할 수 있습니다.

      • 여러가지 인자값만 받을거라면 ArgumentsAccessor를 매개 변수를 통해 받으면 됩니다.

      • ArgumentsAggregator 인터페이스를 구현한 커스텀 Accessor를 만들어서 변환이 가능합니다

        • 그 후 @AggregateWith를 통해 적용시키면 됩니다.

// @RepeatedTest 예제 입니다. 

@DisplayName("Study Test")
@RepeatedTest(value = 10, name = "{displayName}, {currentRepetition} / {totalRepetitions}")
void testRepeatedTest(RepetitionInfo repetitionInfo) {
	System.out.println("Repeated Test: " + repetitionInfo.getCurrentRepetition());
 }
 
 // @ParameterizedTest 예제 입니다.

@DisplayName("Study Test!! Order 3")
@ParameterizedTest(name = "{displayName} {index} {argumentsWithNames}")
@ValueSource(strings = {"test1", "Test2", "Test3"})
void testParameterizedTest1(String message){
        System.out.println(message);
}

// @ParameterizedTest 명시적인 변환 예제입니다. 

@DisplayName("Study Test!! Order 4")
@ParameterizedTest(name = "{displayName} {index} {argumentsWithNames}")
@ValueSource(ints = {10,20,30})
void testParameterizedTest2(@ConvertWith(StudyConverter.class) Study study){
        System.out.println(study.getLimit());
}

static class StudyConverter extends SimpleArgumentConverter{
        @Override
        protected Object convert(Object source, Class<?> target) throws ArgumentConversionException {
            assertEquals(Study.class, target, "Can Only Converter to Study");
            return new Study(Integer.parseInt(source.toString()));
        }
}

// @ParameterizedTest ArgumentsAccessor를 이용한 객체 조합 변환 예제입니다. 

@DisplayName("Study Test!!")
@ParameterizedTest(name = "{displayName} {index} {argumentsWithNames}")
@CsvSource(value = {"10, 자바 스터디, 20, 스프링"})
void testParameterizedTest3(@AggregateWith(StudyAggregator.class) Study study){
        System.out.println(study.toString());
}

static class StudyAggregator implements ArgumentsAggregator{
        @Override
        public Object aggregateArguments(ArgumentsAccessor argumentsAccessor, ParameterContext parameterContext) throws ArgumentsAggregationException {
            return new Study(argumentsAccessor.getInteger(0), argumentsAccessor.getString(1));
        }
 }

JUnit 5. 테스트 인스턴스

JUnit은 테스트 메소드 마다 테스트 인스턴스를 새로 만드는게 기본 전략입니다.

이 이유는 테스트 메소드를 독립적으로 실행하여 예상치 못한 부작용을 방지하기 위함입니다.

이 전략을 JUnit 5에서 @TestInstance(Lifecycle.PER_CLASS)를 통해 변경할 수 있습니다.

  • @TestInstance(Lifecycle.PER_CLASS)

    • 테스트 클래스당 인스턴스를 하나만 만들어 사용합니다. 그러므로 Stateful하게 사용할 수 있습니다.

    • 경우에 따라, 테스트 간에 공유하는 모든 상태를 @BeforeEach 또는 @AfterEach에서 초기화 할 필요가 있습니다.


JUnit 5. 테스트 순서

경우에 따라, 특정 순서대로 테스트를 실행하고 싶을 때가 있습니다. 이런 경우에는 테스트 메소드를 원하는 순서에 따라 실행하도록

@TestInstance(Lifecycle.PER_CLASS)와 함께 @TestMethodOrder를 통해 사용할 수 있습니다.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudyTest {
   
    @Order(1) // 첫번째로 테스트를 실행합니다. 
    @Test
    void test1(){
       ...
    }
    
    @Order(2) // 두번째로 테스트를 실행합니다. 
    @Test
    void test2(){
       ...
    }
    
    @Order(3) // 세번째로 테스트를 실행합니다. 
    @Test
    void test3(){
       ...
    }
}
profile
좋은 습관을 가지고 싶은 평범한 개발자입니다.

0개의 댓글