[스트림] 연습문제
01_computeSumOfAllElements
문제
Integer 타입을 요소로 가지는 List를 입력받아 요소의 총 합을 리턴해야 합니다.
입력
인자 1 : list
- Integer 타입을 요소로 가지는 List
출력
- int 타입을 리턴해야 합니다.
주의 사항
- 비어있는 List의 경우 0을 리턴합니다.
- 반복문(for, while)의 사용은 금지됩니다.
입출력 예시
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int output = computeSumOfAllElements(list);
System.out.println(output); // --> 15
힌트
- stream을 통해 List의 요소를 순회할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class A_computeSumOfAllElements {
public int computeSumOfAllElements(List<Integer> list) {
// TODO:
int output = 0;
if (list.isEmpty()) {
return 0;
}
output = list.stream()
.mapToInt(n -> n)
.sum();
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class A_computeSumOfAllElements {
public int computeSumOfAllElements(List<Integer> list) {
// TODO:
return list.stream()
.mapToInt(number -> number)
.sum();
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class A_computeSumOfAllElementsTest {
A_computeSumOfAllElements test = spy(A_computeSumOfAllElements.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/A_computeSumOfAllElements.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("[1, 2, 3, 4, 5]의 요소를 가진 List를 입력받은 경우, 15를 리턴해야 합니다")
public void test_1() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
assertThat(test.computeSumOfAllElements(list)).isEqualTo(15);
}
@Test
@DisplayName("[5, 8, 10, 4, 3]의 요소를 가진 List를 입력받은 경우, 30를 리턴해야 합니다")
public void test_2() {
List<Integer> list = Arrays.asList(5, 8, 10, 4, 3);
assertThat(test.computeSumOfAllElements(list)).isEqualTo(30);
}
@Test
@DisplayName("[-1, -3, 5, 8, -10]의 요소를 가진 List를 입력받은 경우, -1를 리턴해야 합니다")
public void test_3() {
List<Integer> list = Arrays.asList(-1, -3, 5, 8, -10);
assertThat(test.computeSumOfAllElements(list)).isEqualTo(-1);
}
@Test
@DisplayName("[]와 같이 비어있는 List를 입력받은 경우, 0를 리턴해야 합니다")
public void test_4() {
List<Integer> list = Arrays.asList();
assertThat(test.computeSumOfAllElements(list)).isEqualTo(0);
}
}
02_computeAverageOfNumbers
문제
Integer 타입을 요소로 가지는 List를 입력받아 요소의 평균을 리턴해야합니다.
입력
인자 1 : list
- Integer 타입을 요소로 가지는 List
출력
- double 타입을 리턴해야 합니다.
주의 사항
- 비어있는 List의 경우 0을 리턴해야 합니다.
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
double output = computeAverageOfNumbers(list);
System.out.println(output); // --> 3
힌트
- stream을 통해 List의 요소를 순회할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
public class B_computeAverageOfNumbers {
public double computeAverageOfNumbers(List<Integer> list) {
// TODO:
return list.isEmpty() ? 0 : list.stream()
.mapToDouble(n -> n)
.average()
.getAsDouble();
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class B_computeAverageOfNumbers {
public double computeAverageOfNumbers(List<Integer> list) {
// TODO:
return list.stream()
.mapToDouble(m -> m)
.average()
.orElse(0.0);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class B_computeAverageOfNumbersTest {
B_computeAverageOfNumbers solution = spy(B_computeAverageOfNumbers.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/B_computeAverageOfNumbers.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("[1, 2, 3, 4, 5]의 요소를 가진 List를 입력받은 경우, 3.0을 리턴해야 합니다")
public void test_1() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
assertThat(solution.computeAverageOfNumbers(list)).isEqualTo(3.0);
}
@Test
@DisplayName("[]와 같이 비어있는 List를 입력받은 경우, 0.0을 리턴해야 합니다")
public void test_2() {
List<Integer> list = Arrays.asList();
assertThat(solution.computeAverageOfNumbers(list)).isEqualTo(0);
}
@Test
@DisplayName("[1, 10, 9, 5, 17]의 요소를 가진 List를 입력받은 경우, 8.4을 리턴해야 합니다")
public void test_3() {
List<Integer> list = Arrays.asList(1, 10, 9, 5, 17);
assertThat(solution.computeAverageOfNumbers(list)).isEqualTo(8.4);
}
@Test
@DisplayName("[4, -2, 6, 13, -6, 3, 24]의 요소를 가진 List를 입력받은 경우, 6.0을 리턴해야 합니다")
public void test_4() {
List<Integer> list = Arrays.asList(4, -2, 6, 13, -6, 3, 24);
assertThat(solution.computeAverageOfNumbers(list)).isEqualTo(6.0);
}
@Test
@DisplayName("[5, 10, 15, 20, 25, -50]의 요소를 가진 List를 입력받은 경우, 4.166666666666667을 리턴해야 합니다")
public void test_5() {
List<Integer> list = Arrays.asList(5, 10, 15, 20, 25, -50);
assertThat(solution.computeAverageOfNumbers(list)).isEqualTo(4.166666666666667);
}
}
03_filterOddNumbers
문제
Integer 타입을 요소로 가지는 List를 입력받이 짝수 요소만 추출한 List를 리턴해야 합니다.
입력
인자 1 : list
- Integer 타입을 요소로 가지는 List
출력
- Integer타입을 요소로 가지는 List를 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> output = computeAverageOfNumbers(list);
System.out.println(output); // --> [2, 4]
힌트
- stream을 통해 List의 요소를 순회할 수 있습니다.
- 특정 조건에 맞는 요소만 구해야할 때, filter를 이용하면 반복문과 조건문을 대체할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
import java.util.stream.*;
public class C_filterOddNumbers {
public List<Integer> filterOddNumbers(List<Integer> list) {
// TODO:
return list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
import java.util.stream.*;
public class C_filterOddNumbers {
public List<Integer> filterOddNumbers(List<Integer> list) {
// TODO:
return list.stream()
.filter(el -> el % 2 == 0)
.collect(Collectors.toList());
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class C_filterOddNumbersTest {
C_filterOddNumbers solution = spy(C_filterOddNumbers.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/C_filterOddNumbers.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("[1, 2, 3, 4, 5]의 요소를 가진 List를 입력받은 경우, List [2, 4]를 리턴해야 합니다.")
public void test_1() {
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> list2 = Arrays.asList(2, 4);
String result = solution.filterOddNumbers(list1).toString();
assertThat(result).isEqualTo(list2.toString());
}
@Test
@DisplayName("[-4, -2, 0, 2, 4]의 요소를 가진 List를 입력받은 경우, List [-4, -2, 0, 2, 4]를 리턴해야 합니다.")
public void test_2() {
List<Integer> list1 = Arrays.asList(-4, -2, 0, 2, 4);
List<Integer> list2 = Arrays.asList(-4, -2, 0, 2, 4);
String result = solution.filterOddNumbers(list1).toString();
assertThat(result).isEqualTo(list2.toString());
}
@Test
@DisplayName("[]와 같이 비어있는 List를 입력받은 경우, 비어있는 List []를 리턴해야 합니다.")
public void test_3() {
List<Integer> list1 = Arrays.asList();
List<Integer> list2 = Arrays.asList();
String result = solution.filterOddNumbers(list1).toString();
assertThat(result).isEqualTo(list2.toString());
}
@Test
@DisplayName("[1, 3, 5, 7, 9]의 요소를 가진 List를 입력받은 경우, 비어있는 List []를 리턴해야 합니다.")
public void test_4() {
List<Integer> list1 = Arrays.asList(1, 3, 5, 7, 9);
List<Integer> list2 = Arrays.asList();
String result = solution.filterOddNumbers(list1).toString();
assertThat(result).isEqualTo(list2.toString());
}
@Test
@DisplayName("[1024, 937, 1724, 372, 1928]의 요소를 가진 List를 입력받은 경우, [1024, 1724, 372, 1928]를 리턴해야 합니다.")
public void test_5() {
List<Integer> list1 = Arrays.asList(1024, 937, 1724, 372, 1928);
List<Integer> list2 = Arrays.asList(1024, 1724, 372, 1928);
String result = solution.filterOddNumbers(list1).toString();
assertThat(result).isEqualTo(list2.toString());
}
}
04_computeCountOfFemaleMember
문제
Member 클래스를 이용해 회원의 이름과 성별을 관리하려고 합니다. Member타입의 List를 입력받아, 여성 회원의 수를 리턴해야 합니다.
입력
인자 1 : members
- Member 타입을 요소로 가지는 List
출력
- long 타입을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
Member coding = new Member("coding", "Female");
Member hacker = new Member("hacker", "Male");
List<Member> members = Arrays.asList(coding, hacker);
long output = computeCountOfFemaleMember(members);
System.out.println(output); // --> 1
힌트
- List의 각 요소는 Member 클래스의 인스턴스 객체입니다.
- 각 요소(인스턴스 객체)에서 성별을 어떻게 알아낼 수 있을지 Member 객체의 메서드를 살펴보세요.
내 코드
package com.choongang;
import java.util.*;
import java.util.stream.Collectors;
public class D_computeCountOfFemaleMember {
public long computeCountOfFemaleMember(List<Member> members){
// TODO:
return members.stream()
.filter(Member -> Member.getGender().equals("Female"))
.count();
}
static class Member {
String name;
String gender;
public Member(String name, String gender) {
this.name = name;
this.gender = gender;
}
public String getName() {
return name;
}
public String getGender() {
return gender;
}
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
import java.util.stream.Collectors;
public class D_computeCountOfFemaleMember {
public long computeCountOfFemaleMember(List<Member> members){
// TODO:
// 여자만 체크해서 여자의 총 수를 구해 반환 (long)
return members.stream()
.filter(member -> member.getGender().equals("Female"))
.count();
}
static class Member {
String name;
String gender;
public Member(String name, String gender) {
this.name = name;
this.gender = gender;
}
public String getName() {
return name;
}
public String getGender() {
return gender;
}
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class D_computeCountOfFemaleMemberTest {
D_computeCountOfFemaleMember solution = spy(D_computeCountOfFemaleMember.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/D_computeCountOfFemaleMember.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("성별이 Female인 회원의 수가 2명인 경우, 2를 리턴해야 합니다")
public void test_1() {
D_computeCountOfFemaleMember.Member coding = new D_computeCountOfFemaleMember.Member("coding", "Female");
D_computeCountOfFemaleMember.Member hacker = new D_computeCountOfFemaleMember.Member("hacker", "Male");
D_computeCountOfFemaleMember.Member java = new D_computeCountOfFemaleMember.Member("java", "Female");
List<D_computeCountOfFemaleMember.Member> members = Arrays.asList(coding, hacker, java);
assertThat(solution.computeCountOfFemaleMember(members)).isEqualTo(2);
}
@Test
@DisplayName("성별이 Female인 회원의 수가 4명 인 경우, 4를 리턴해야 합니다")
public void test_2() {
D_computeCountOfFemaleMember.Member coding = new D_computeCountOfFemaleMember.Member("coding", "Female");
D_computeCountOfFemaleMember.Member hacker = new D_computeCountOfFemaleMember.Member("hacker", "Male");
D_computeCountOfFemaleMember.Member java = new D_computeCountOfFemaleMember.Member("java", "Female");
D_computeCountOfFemaleMember.Member js = new D_computeCountOfFemaleMember.Member("js", "Female");
D_computeCountOfFemaleMember.Member code = new D_computeCountOfFemaleMember.Member("code", "Female");
List<D_computeCountOfFemaleMember.Member> members = Arrays.asList(coding, hacker, java, js, code);
assertThat(solution.computeCountOfFemaleMember(members)).isEqualTo(4);
}
@Test
@DisplayName("회원의 성별이 모두 남자인 경우, 0을 리턴해야 합니다")
public void test_3() {
D_computeCountOfFemaleMember.Member coding = new D_computeCountOfFemaleMember.Member("coding", "Male");
D_computeCountOfFemaleMember.Member hacker = new D_computeCountOfFemaleMember.Member("hacker", "Male");
D_computeCountOfFemaleMember.Member java = new D_computeCountOfFemaleMember.Member("java", "Male");
List<D_computeCountOfFemaleMember.Member> members = Arrays.asList(coding, hacker, java);
assertThat(solution.computeCountOfFemaleMember(members)).isEqualTo(0);
}
@Test
@DisplayName("회원의 수가 0명인 경우, 0을 리턴해야 합니다")
public void test_4() {
List<D_computeCountOfFemaleMember.Member> members = Arrays.asList();
assertThat(solution.computeCountOfFemaleMember(members)).isEqualTo(0);
}
}
05_computeAverageOfMaleMember
문제
이번엔 Member 클래스를 이용해 회원의 이름, 성별, 나이까지 관리하려고 합니다. Member 타입을 요소로 가지는 List를 입력받아, 남성 회원들의 평균 나이를 리턴해야 합니다.
입력
인자 1 : members
- Member 타입을 요소로 가지는 List
출력
- double 타입을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 회원의 수가 0이거나, 남자 회원이 없는 경우 0을 리턴해야 합니다.
입출력 예시
Member coding = new Member("coding", "Female", 25);
Member hacker = new Member("hacker", "Male", 30);
Member ingi = new Member("ingi", "Male", 32);
List<Member> members = Arrays.asList(coding, hacker, ingi);
double output = computeAverageOfMaleMember(members);
System.out.println(output); // --> 31.0
힌트
- List의 각 요소는 Member 클래스의 인스턴스 객체입니다.
- 각 요소(인스턴스 객체)에서 성별을 어떻게 알아낼 수 있을지 Member 객체의 메서드를 살펴보세요.
내 코드
package com.choongang;
import java.util.*;
public class E_computeAverageOfMaleMember {
public double computeAverageOfMaleMember(List<Member> members) {
// TODO:
return members.stream()
.filter(n -> n.getGender().equals("Male"))
.mapToInt(n -> n.getAge())
.average()
.orElse(0.0);
}
static class Member {
String name;
String gender;
int age;
public Member(String name, String gender, int age) {
this.name = name;
this.gender = gender;
this.age = age;
}
public String getName() {
return name;
}
public String getGender() {
return gender;
}
public int getAge() {
return age;
}
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class E_computeAverageOfMaleMember {
public double computeAverageOfMaleMember(List<Member> members) {
// TODO:
return members.stream()
.filter(member -> member.getGender().equals("Male"))
.mapToInt(member -> member.getAge())
.average()
.orElse(0.0);
}
static class Member {
String name;
String gender;
int age;
public Member(String name, String gender, int age) {
this.name = name;
this.gender = gender;
this.age = age;
}
public String getName() {
return name;
}
public String getGender() {
return gender;
}
public int getAge() {
return age;
}
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class E_computeAverageOfMaleMemberTest {
E_computeAverageOfMaleMember solution = spy(E_computeAverageOfMaleMember.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/E_computeAverageOfMaleMember.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("입력받은 두 명의 남자 회원의 나이가 [30, 32]세 라면, 31.0을 리턴해야 합니다")
public void test_1() {
E_computeAverageOfMaleMember.Member coding = new E_computeAverageOfMaleMember.Member("coding", "Female", 25);
E_computeAverageOfMaleMember.Member hacker = new E_computeAverageOfMaleMember.Member("hacker", "Male", 30);
E_computeAverageOfMaleMember.Member ingi = new E_computeAverageOfMaleMember.Member("ingi", "Male", 32);
List<E_computeAverageOfMaleMember.Member> members = Arrays.asList(coding, hacker, ingi);
assertThat(solution.computeAverageOfMaleMember(members)).isEqualTo(31.0);
}
@Test
@DisplayName("입력받은 남자 회원의 나이가 [30, 32, 27, 50]세 라면, 34.75을 리턴해야 합니다")
public void test_2() {
E_computeAverageOfMaleMember.Member coding = new E_computeAverageOfMaleMember.Member("coding", "Male", 30);
E_computeAverageOfMaleMember.Member hacker = new E_computeAverageOfMaleMember.Member("hacker", "Male", 32);
E_computeAverageOfMaleMember.Member ingi = new E_computeAverageOfMaleMember.Member("ingi", "Male", 27);
E_computeAverageOfMaleMember.Member code = new E_computeAverageOfMaleMember.Member("code", "Male", 50);
List<E_computeAverageOfMaleMember.Member> members = Arrays.asList(coding, hacker, ingi, code);
assertThat(solution.computeAverageOfMaleMember(members)).isEqualTo(34.75);
}
@Test
@DisplayName("남자 회원이 없는 경우, 0을 리턴해야 합니다")
public void test_3() {
E_computeAverageOfMaleMember.Member coding = new E_computeAverageOfMaleMember.Member("coding", "Female", 30);
E_computeAverageOfMaleMember.Member hacker = new E_computeAverageOfMaleMember.Member("hacker", "Female", 32);
List<E_computeAverageOfMaleMember.Member> members = Arrays.asList(coding, hacker);
assertThat(solution.computeAverageOfMaleMember(members)).isEqualTo(0);
}
@Test
@DisplayName("회원의 수가 0인 경우, 0을 리턴해야 합니다")
public void test_4() {
List<E_computeAverageOfMaleMember.Member> members = Arrays.asList();
assertThat(solution.computeAverageOfMaleMember(members)).isEqualTo(0);
}
}
06_makeUniqueNameArray
문제
String 타입을 요소로 가지는 List를 입력받아 중복을 제거하고 정렬한 후 String 타입을 요소로 갖는 배열로 리턴해야 합니다.
입력
인자 1 : names
- String 타입을 요소로 가지는 List
출력
- String 타입을 요소로 가지는 배열을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 정렬은 사전식 순서로 정렬 되어야 합니다.
입출력 예시
List<String> names = Arrays.asList("김코딩", "박해커", "김코딩", "최자바", "박해커");
String[] output = makeUniqueNameArray(names);
System.out.println(output); // {"김코딩", "박해커", "최자바"};
힌트
- stream을 통해 List의 요소를 순회할 수 있습니다.
- stream의 중간 연산으로 중복을 제거할 수 있습니다.
- stream의 중간 연산으로 정렬을 할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class F_makeUniqueNameArray {
public String[] makeUniqueNameArray(List<String> names) {
// TODO:
return names.stream()
.distinct()
.sorted()
.toArray(String[]::new);
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class F_makeUniqueNameArray {
public String[] makeUniqueNameArray(List<String> names) {
// TODO:
// list -> stream
// 중복 제거
// 정렬 (오름차순)
// 배열로 변환
// 리턴
return names.stream()
.distinct()
.sorted()
.toArray(String[]::new);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class F_makeUniqueNameArrayTest {
F_makeUniqueNameArray solution = spy(F_makeUniqueNameArray.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/F_makeUniqueNameArray.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("[\"김코딩\", \"박해커\", \"김코딩\", \"최자바\", \"박해커\"]의 요소를 가진 List를 입력받은 경우, String 타입을 요소로 갖는 배열 {\"김코딩\", \"박해커\", \"최자바\"}을 리턴해야 합니다")
public void test_1() {
List<String> names = Arrays.asList("김코딩", "박해커", "김코딩", "최자바", "박해커");
assertThat(solution.makeUniqueNameArray(names)).isEqualTo(new String[]{"김코딩", "박해커", "최자바"});
}
@Test
@DisplayName("[\"김코드\", \"이스프링\", \"김코딩\", \"최자바\", \"박해커\"]의 요소를 가진 List를 입력받은 경우, String 타입을 요소로 갖는 배열 {\"김코드\", \"김코딩\", \"박해커\", \"이스프링\", \"최자바\"}을 리턴해야 합니다")
public void test_2() {
List<String> names = Arrays.asList("김코드", "이스프링", "김코딩", "최자바", "박해커");
assertThat(solution.makeUniqueNameArray(names)).isEqualTo(new String[]{"김코드", "김코딩", "박해커", "이스프링", "최자바"});
}
@Test
@DisplayName("[\"백엔드\", \"프론트엔드\", \"백엔드\", \"풀스택\"]의 요소를 가진 List를 입력받은 경우, String 타입을 요소로 갖는 배열 {\"백엔드\", \"풀스택\", \"프론트엔드\"}을 리턴해야 합니다")
public void test_3() {
List<String> names = Arrays.asList("백엔드", "프론트엔드", "백엔드", "풀스택");
assertThat(solution.makeUniqueNameArray(names)).isEqualTo(new String[]{"백엔드", "풀스택", "프론트엔드"});
}
@Test
@DisplayName("[]와 같이 비어있는 List를 입력받은 경우, 빈 배열을 리턴해야 합니다")
public void test_4() {
assertThat(solution.makeUniqueNameArray(Arrays.asList())).isEqualTo(new String[]{});
}
}
07_filterName
문제
String타입을 요소로 가지는 List를 입력받아 중복을 제거하고 김씨 성을 가진 이름들을 정렬하여 문자열 배열로 리턴해야 합니다.
입력
인자 1 : names
- String 타입을 요소로 가지는 List
출력
- String 타입을 요소로 가지는 배열을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 입력받은 리스트에 김씨가 없거나, 리스트의 크기가 0인 경우 빈 배열을 리턴해야 합니다.
- 정렬은 사전식 순서로 정렬 되어야 합니다.
입출력 예시
List<String> names = Arrays.asList("김코딩", "박해커", "김코딩", "최자바", "김자바");
String[] output = filterName(names);
System.out.println(output); // {"김자바", "김코딩"};
힌트
- stream을 통해 List의 요소를 순회할 수 있습니다.
- stream의 중간 연산으로 중복을 제거할 수 있습니다.
- stream의 중간 연산으로 정렬을 할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
public class G_filterName {
public String[] filterName(List<String> names) {
// TODO:
return names.stream()
.distinct()
.filter(n -> n.startsWith("김"))
.sorted()
.toArray(String[]::new);
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class G_filterName {
public String[] filterName(List<String> names) {
// TODO:
// list -> stream
// 중복 제거
//김씨 성을 가진 사람만 골라야 함
// 정렬 (오름차순)
// 배열로 변환
// 리턴
return names.stream()
.distinct()
.filter(str -> str.startsWith("김"))
.sorted()
.toArray(String[]::new);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class G_filterNameTest {
G_filterName solution = spy(G_filterName.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/G_filterName.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("[\"김코딩\", \"박해커\", \"김코딩\", \"최자바\", \"김자바\"]의 요소를 가진 List를 입력받은 경우, String 타입을 요소로 가지는 배열 {\"김자바\", \"김코딩\"}을 리턴해야 합니다.")
public void test_1() {
List<String> names = Arrays.asList("김코딩", "박해커", "김코딩", "최자바", "김자바");
assertThat(solution.filterName(names)).isEqualTo(new String[]{"김자바", "김코딩"});
}
@Test
@DisplayName("[\"김코딩\", \"김해커\", \"김자바\", \"김코드\", \"김코츠\"]의 요소를 가진 List를 입력받은 경우, String 타입을 요소로 가지는 배열 {\"김자바\", \"김코드\", \"김코딩\", \"김코츠\", \"김해커\"}을 리턴해야 합니다.")
public void test_2() {
List<String> names = Arrays.asList("김코딩", "김해커", "김자바", "김코드", "김코츠");
assertThat(solution.filterName(names)).isEqualTo(new String[]{"김자바", "김코드", "김코딩", "김코츠", "김해커"});
}
@Test
@DisplayName("[\"코딩김\", \"해커박\", \"코드김\", \"자바최\"]의 요소를 가진 List를 입력받은 경우, 빈 배열을 리턴해야 합니다")
public void test_3() {
List<String> names = Arrays.asList("코딩김", "해커박", "코드김", "자바최");
assertThat(solution.filterName(names)).isEqualTo(new String[]{});
}
@Test
@DisplayName("[]와 같이 비어있는 List를 입력받은 경우, 빈 배열을 리턴해야 합니다")
public void test_4() {
List<String> names = Arrays.asList();
assertThat(solution.filterName(names)).isEqualTo(new String[]{});
}
}
08_findBiggestNumber
문제
int 타입을 요소로 가지는 배열을 입력받아 가장 큰 요소를 리턴해야 합니다.
입력
인자 1 : arr
- int 타입을 요소로 가지는 배열
출력
- Integer 타입을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 빈 배열을 입력받은 경우 null을 리턴해야 합니다.
입출력 예시
int[] arr = {1, 10, 5, 32, 5};
Integer output = findBiggestNumber(arr);
System.out.println(output); // 32
힌트
- stream을 통해 배열의 요소를 순회할 수 있습니다.
- stream의 다양한 연산으로 쉽게 원하는 값을 구할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
public class H_findBiggestNumber {
public Integer findBiggestNumber(int[] arr) {
// TODO:
return Arrays.stream(arr).boxed()
.max(Comparator.comparing(x -> x))
.orElse(null);
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class H_findBiggestNumber {
public Integer findBiggestNumber(int[] arr) {
// TODO:
// 배열을 받아서 가장 큰 수를 반환
if (arr.length == 0) {
return null;
}
return Arrays.stream(arr)
.max()
.getAsInt();
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class H_findBiggestNumberTest {
H_findBiggestNumber solution = spy(H_findBiggestNumber.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/H_findBiggestNumber.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("{1, 10, 5, 32, 5}의 요소를 가진 배열을 입력받은 경우, 32를 리턴해야 합니다")
public void test_1() {
int[] arr = {1, 10, 5, 32, 5};
assertThat(solution.findBiggestNumber(arr)).isEqualTo(32);
}
@Test
@DisplayName("{1, 0, -2, -19, 3, -20}의 요소를 가진 배열을 입력받은 경우, 3을 리턴해야 합니다")
public void test_2() {
int[] arr = {1, 0, -2, -19, 3, -20};
assertThat(solution.findBiggestNumber(arr)).isEqualTo(3);
}
@Test
@DisplayName("{}와 같이 빈 배열을 입력받은 경우, null을 리턴해야 합니다")
public void test_3() {
int[] arr = {};
assertThat(solution.findBiggestNumber(arr)).isNull();
}
@Test
@DisplayName("{-10, -8, -2, -6, -3}의 요소를 가진 배열을 입력받은 경우, -2을 리턴해야 합니다")
public void test_4() {
int[] arr = {-10, -8, -2, -6, -3};
assertThat(solution.findBiggestNumber(arr)).isEqualTo(-2);
}
}
09_findLongestLength
문제
String 타입을 요소로 가지는 배열을 입력받아, 가장 길이가 긴 문자열 요소의 길이를 리턴해야 합니다.
입력
인자 1 : strArr
- String 타입을 요소로 가지는 배열
출력
- int 타입을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 빈 배열을 입력받은 경우 0을 리턴해야 합니다.
입출력 예시
String[] strArr = {"codestates", "java", "backend", "programming"};
int output = findLongestLength(strArr);
System.out.println(output); // 11 (가장 긴 문자열 : programming)
힌트
- stream을 통해 배열의 요소를 순회할 수 있습니다.
- stream을 통해 요소의 길이를 기준으로 순회할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
public class I_findLongestLength {
public int findLongestLength(String[] strArr) {
// TODO:
return Arrays.stream(strArr)
.mapToInt(String::length)
.max()
.orElse(0);
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class I_findLongestLength {
public int findLongestLength(String[] strArr) {
// TODO:
// 배열에 문자열이 여러개 요소로 있음
// 여기서 가장 긴 문자열의 길이를 반환해야 함
// 스트림을 만들고
// max() 사용 -> 스트림의 요소가 숫자여야 하겠군!
// 문자열을 순회하면서 각 요소를 문자열의 길이로 바꾼 후,
// max() 사용해서 가장 큰 수를 반환하면 되겠구나!
return Arrays.stream(strArr)
.mapToInt(String::length)
.max()
.orElse(0);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class I_findLongestLengthTest {
I_findLongestLength solution = spy(I_findLongestLength.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/I_findLongestLength.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("{\"codecodcod\", \"java\", \"backend\", \"programming\"}의 요소를 가진 String 타입을 요소로 가지는 배열을 입력받은 경우, 11을 리턴해야 합니다")
public void test_1() {
String[] strArr = {"codecodcod", "java", "backend", "programming"};
assertThat(solution.findLongestLength(strArr)).isEqualTo(11);
}
@Test
@DisplayName("{}와 같이 빈 배열을 입력받은 경우, 0을 리턴해야 합니다")
public void test_2() {
String[] strArr = {};
assertThat(solution.findLongestLength(strArr)).isEqualTo(0);
}
@Test
@DisplayName("{\"a\", \"b\", \"c\", \"d\", \"e\"}의 요소를 가진 String 타입을 요소로 가지는 배열을 입력받은 경우, 1을 리턴해야 합니다")
public void test_3() {
String[] strArr = {"a", "b", "c", "d", "e"};
assertThat(solution.findLongestLength(strArr)).isEqualTo(1);
}
@Test
@DisplayName("{\"coding\", \"is\", \"exciting\", \"!\"}의 요소를 가진 String 타입을 요소로 가지는 배열을 입력받은 경우, 8을 리턴해야 합니다")
public void test_4() {
String[] strArr = {"coding", "is", "exciting", "!"};
assertThat(solution.findLongestLength(strArr)).isEqualTo(8);
}
}
10_mergeTwoStream
문제
String 타입을 요소로 가지는 List 두 개를 입력받아, 스트림을 이용해 하나의 List로 합친 결과를 리턴해야 합니다.
입력
인자 1 : list1
- String 타입을 요소로 가지는 List
인자 2 : list2
- String 타입을 요소로 가지는 List
출력
- String 타입을 요소로 가지는 List를 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
List<String> list1 = Arrays.asList("김코딩", "박해커");
List<String> list2 = Arrays.asList("최자바", "이스프링");
List<String> output = mergeTwoStream(list1, list2);
System.out.println(output); // ["김코딩", "박해커", "최자바", "이스프링"]
힌트
- 각 리스트의 스트림을 생성한 후 두 개의 스트림을 하나로 붙일 수(concatenate) 있습니다.
- Stream의 최종 연산 단계에서 리스트의 형태로 만들 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
import java.util.stream.*;
public class J_mergeTwoStream {
public List<String> mergeTwoStream(List<String> list1, List<String> list2) {
// TODO:
Stream<String> stringStream1 = list1.stream();
Stream<String> stringStream2 = list2.stream();
Stream<String> concat = Stream.concat(stringStream1, stringStream2);
String[] array = concat.toArray(String[]::new);
List<String> list = Arrays.asList(array);
return list;
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
import java.util.stream.*;
public class J_mergeTwoStream {
public List<String> mergeTwoStream(List<String> list1, List<String> list2) {
// TODO:
return Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toList());
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class J_mergeTwoStreamTest {
J_mergeTwoStream solution = spy(J_mergeTwoStream.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/J_mergeTwoStream.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("[\"김코딩\", \"박해커\"] 와 [\"최자바\", \"이스프링\"] 두 개의 List를 입력받는 경우, [\"김코딩\", \"박해커\", \"최자바\", \"이스프링\"] List를 리턴해야 합니다.")
public void test_1() {
List<String> list1 = Arrays.asList("김코딩", "박해커");
List<String> list2 = Arrays.asList("최자바", "이스프링");
assertThat(solution.mergeTwoStream(list1, list2)).isEqualTo(Arrays.asList("김코딩", "박해커", "최자바", "이스프링"));
}
@Test
@DisplayName("[\"김코딩\", \"김코딩\", \"김코딩\"] 와 [\"김코딩\", \"이스프링\"] 두 개의 List를 입력받는 경우, [\"김코딩\", \"김코딩\", \"김코딩\", \"김코딩\", \"이스프링\"] List를 리턴해야 합니다.")
public void test_2() {
List<String> list1 = Arrays.asList("김코딩", "김코딩", "김코딩");
List<String> list2 = Arrays.asList("김코딩", "이스프링");
List<String> result = Arrays.asList("김코딩", "김코딩", "김코딩", "김코딩", "이스프링");
assertThat(solution.mergeTwoStream(list1, list2)).isEqualTo(result);
}
@Test
@DisplayName("[] 와 [] 처럼 두 개의 비어있는 List를 입력받는 경우, [] 비어있는 List를 리턴해야 합니다.")
public void test_3() {
List<String> list1 = Arrays.asList();
List<String> list2 = Arrays.asList();
assertThat(solution.mergeTwoStream(list1, list2)).isEqualTo(Arrays.asList());
}
@Test
@DisplayName("[\"code\", \"spring\"] 와 [\"BackEnd\", \"course\"] 두 개의 List를 입력받는 경우, [\"code\", \"spring\", \"BackEnd\", \"course\"] 비어있는 List를 리턴해야 합니다.")
public void test_4() {
List<String> list1 = Arrays.asList("code", "spring");
List<String> list2 = Arrays.asList("BackEnd", "course");
assertThat(solution.mergeTwoStream(list1, list2)).isEqualTo(Arrays.asList("code", "spring", "BackEnd", "course"));
}
}
11_makeElementDouble
문제
Integer 타입을 요소로 가지는 List를 입력받아 각 요소에 2를 곱한 새로운 List를 리턴해야 합니다.
입력
인자 1 : list
- Integer 타입을 요소로 가지는 List
출력
- Integer 타입을 요소로 가지는 List를 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 비어있는 리스트를 입력받은 경우, 비어있는 리스트를 리턴해야 합니다.
입출력 예시
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> output = makeElementDouble(list);
System.out.println(output); // [2, 4, 6, 8, 10]
힌트
- Stream을 통해 List를 순회하며 각 요소에 동일한 연산을 반복할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
import java.util.stream.*;
public class K_makeElementDouble {
public List<Integer> makeElementDouble(List<Integer> list) {
// TODO:
return list.stream()
.map(el -> el * 2)
.collect(Collectors.toList());
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
import java.util.stream.*;
public class K_makeElementDouble {
public List<Integer> makeElementDouble(List<Integer> list) {
// TODO:
return list.stream()
.map(el -> el * 2)
.collect(Collectors.toList());
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class K_makeElementDoubleTest {
K_makeElementDouble solution = spy(K_makeElementDouble.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/K_makeElementDouble.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("[1, 2, 3, 4, 5]의 요소를 가진 List를 입력받은 경우, [2, 4, 6, 8, 10] List를 리턴해야 합니다")
public void test_1() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
assertThat(solution.makeElementDouble(list)).isEqualTo(Arrays.asList(2, 4, 6, 8, 10));
}
@Test
@DisplayName("[0, 3, 5, 8]의 요소를 가진 List를 입력받은 경우, [0, 6, 10, 16] List를 리턴해야 합니다")
public void test_2() {
List<Integer> list = Arrays.asList(0, 3, 5, 8);
assertThat(solution.makeElementDouble(list)).isEqualTo(Arrays.asList(0, 6, 10, 16));
}
@Test
@DisplayName("[]와 같이 비어있는 List를 입력받은 경우, [] List를 리턴해야 합니다")
public void test_3() {
List<Integer> list = Arrays.asList();
assertThat(solution.makeElementDouble(list)).isEqualTo(Arrays.asList());
}
@Test
@DisplayName("[2, -9, -10, -15]의 요소를 가진 List를 입력받은 경우, [-4, -18, -20, -30] List를 리턴해야 합니다")
public void test_4() {
List<Integer> list = Arrays.asList(-2, -9, -10, -15);
assertThat(solution.makeElementDouble(list)).isEqualTo(Arrays.asList(-4, -18, -20, -30));
}
}
12_isHot
문제
이번주의 최고 온도만을 모아놓은 List를 분석하여 이번주가 더웠는지 알아봅시다. 최고 기온이 30도를 넘은 날이 3일 이상이면 true를, 그렇지 않다면 false를 리턴해야 합니다.
입력
인자 1 : temperature
- int 타입을 요소로 가지며 길이가 7인 배열
출력
- boolean 타입을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 30도 이상은 30도를 포함합니다.
- 입력받은 배열의 길이가 7이 아닌 경우 false를 리턴해야 합니다.
입출력 예시
int[] temperature = {25, 29, 30, 31, 26, 30, 33};
boolean output = isHot(temperature);
System.out.println(output); // true
힌트
- Stream을 통해 각 요소가 특정 숫자 이상인 경우의 셀 수 있습니다.
- Stream의 최종 연산 후 결과를 다른 연산에 사용할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
public class L_isHot {
public boolean isHot(int[] temperature) {
// TODO:
long num = Arrays.stream(temperature)
.filter(n -> n >= 30)
.count();
if (num >= 3) {
return true;
}
return false;
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class L_isHot {
public boolean isHot(int[] temperature) {
// TODO:
// 배열 내의 숫자는 온도, 그 중 30이 넘은 것을 남겨야 함
// 남은 요소의 갯수가 3개 이상인 경우 true else false;
long hotDays = Arrays.stream(temperature)
.filter(temp -> temp >= 30)
.count();
return hotDays >= 3;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class L_isHotTest {
L_isHot solution = spy(L_isHot.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/L_isHot.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("이번주의 최고 온도를 모아놓은 배열 {25, 29, 30, 31, 26, 30, 33}를 입력받은 경우, true를 리턴해야 합니다")
public void test_1() {
int[] temperature = {25, 29, 30, 31, 26, 30, 33};
assertThat(solution.isHot(temperature)).isTrue();
}
@Test
@DisplayName("이번주의 최고 온도를 모아놓은 배열 {30, 30, 30, 29, 27, 26, 25}를 입력받은 경우, true를 리턴해야 합니다")
public void test_2() {
int[] temperature = {30, 30, 30, 29, 27, 26, 25};
assertThat(solution.isHot(temperature)).isTrue();
}
@Test
@DisplayName("이번주의 최고 온도를 모아놓은 배열 {27, 30, 30, 29, 27, 26, 25}를 입력받은 경우, false를 리턴해야 합니다")
public void test_3() {
int[] temperature = {27, 30, 30, 29, 27, 26, 25};
assertThat(solution.isHot(temperature)).isFalse();
}
@Test
@DisplayName("이번주의 최고 온도를 모아놓은 배열로 빈 배열 {}을 입력받은 경우, false를 리턴해야 합니다")
public void test_4() {
int[] temperature = {};
assertThat(solution.isHot(temperature)).isFalse();
}
}
13_findPeople
문제
남성 회원 이름의 List와 여성 회원 이름의 List가 있습니다. 남성 회원과 여성 회원 중 특정 성씨를 갖고 있는 사람의 명단이 필요합니다. 중복된 이름은 제거하고 특정 성씨를 갖고 있는 회원들의 이름을 정렬한 후 List로 리턴해야 합니다.
입력
인자 1 : male
- String 타입을 요소로 가지는 List
인자 2 : female
- String 타입을 요소로 가지는 List
인자 3 : lastName
- String 타입의 변수
출력
- String 타입을 요소로 가지는 List를 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 일치하는 회원이 없는 경우 비어있는 List를 리턴해야 합니다.
- 중복 요소는 허용하지 않습니다.
- 사전식 순서로 정렬이 필요합니다.
입출력 예시
List<String> male = Arrays.asList("김코딩", "최자바", "김코츠");
List<String> female = Arrays.asList("박해커", "김유클", "김코딩");
List<String> output = findPeople(male, female, "김");
System.out.println(output); // ["김유클", "김코딩", "김코츠"]
힌트
- 각 리스트의 스트림을 생성한 후 두 개의 스트림을 하나로 붙일 수(concatenate) 있습니다.
- stream을 통해 List의 요소를 순회할 수 있습니다.
- stream의 중간 연산으로 중복을 제거할 수 있습니다.
- stream의 중간 연산으로 정렬을 할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
import java.util.stream.*;
public class M_findPeople {
public List<String> findPeople(List<String> male, List<String> female, String lastName) {
// TODO:
Stream<String> maleStream = male.stream();
Stream<String> femaleStream = female.stream();
Stream<String> concat = Stream.concat(maleStream, femaleStream);
return concat.distinct()
.filter(n -> n.startsWith(lastName))
.sorted()
.collect(Collectors.toList());
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
import java.util.stream.*;
public class M_findPeople {
public List<String> findPeople(List<String> male, List<String> female, String lastName) {
// TODO:
return Stream.concat(male.stream(), female.stream())
.distinct()
.filter(people -> people.startsWith(lastName))
.sorted()
.collect(Collectors.toList());
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import java.util.*;
class M_findPeopleTest {
M_findPeople solution = spy(M_findPeople.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)은 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/M_findPeople.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("남성 회원 List [\"김코딩\", \"최자바\", \"김코츠\"]와 여성 회원 List [\"박해커\", \"김유클\", \"김코딩\"], \"김\"씨 성을 입력받은 경우, List [\"김유클\", \"김코딩\", \"김코츠\"]를 리턴해야 합니다")
public void test_1() {
List<String> male = Arrays.asList("김코딩", "최자바", "김코츠");
List<String> female = Arrays.asList("박해커", "김유클", "김코딩");
assertThat(solution.findPeople(male, female, "김")).isEqualTo(Arrays.asList("김유클", "김코딩", "김코츠"));
}
@Test
@DisplayName("남성 회원 List [\"김코딩\", \"최자바\", \"김코츠\"]와 여성 회원 List [\"박해커\", \"김유클\", \"김코딩\"], \"최\"씨 성을 입력받은 경우, List [\"김유클\", \"최자바\"]를 리턴해야 합니다")
public void test_2() {
List<String> male = Arrays.asList("김코딩", "최자바", "김코츠");
List<String> female = Arrays.asList("박해커", "김유클", "김코딩");
assertThat(solution.findPeople(male, female, "최")).isEqualTo(Arrays.asList("최자바"));
}
@Test
@DisplayName("남성 회원 List [\"김코드\", \"최자바\", \"이스프링\"]와 여성 회원 List [\"박해커\", \"김유클\", \"김코딩\"], \"정\"씨 성을 입력받은 경우, 비어있는 List []를 리턴해야 합니다")
public void test_3() {
List<String> male = Arrays.asList("김코드", "최자바", "이스프링");
List<String> female = Arrays.asList("박해커", "김유클", "김코딩");
assertThat(solution.findPeople(male, female, "정")).isEqualTo(Arrays.asList());
}
@Test
@DisplayName("비어있는 남성 회원 List []와 비어있는 여성 회원 List [], \"김\"씨 성을 입력받은 경우, 비어있는 List []를 리턴해야 합니다")
public void test_4() {
List<String> male = Arrays.asList();
List<String> female = Arrays.asList();
assertThat(solution.findPeople(male, female, "김")).isEqualTo(Arrays.asList());
}
}
파일 입출력(I/O)
이번 유닛에서는 파일 입출력을 다루는 방법을 학습합니다.
이번 유닛을 학습하고 나면 파일을 읽고, 쓰고, 생성하고, 삭제할 수 있습니다.
학습 목표
- 바이트 기반 스트림의 간단한 입출력 코드를 이해하고 활용합니다.
- 문자 기반 스트림의 간단한 입출력 코드를 이해하고 활용합니다.
- 파일 클래스를 이해하고 활용합니다.
InputStream, OutputStream
자바에서는 입출력을 다루기 위한 InputStream, OutputStream을 제공합니다. 스트림은 단방향으로만 데이터를 전송할 수 있기에, 입력과 출력을 동시에 처리하기 위해서는 각각의 스트림이 필요합니다.
입출력 스트림은 어떤 대상을 다루느냐에 따라 종류가 나뉩니다. 예를 들면, File을 다룰 때는 FileInputStream / FileOutputStream을 사용하고, 프로세스를 다룰 때는 PipedInputStream / PipedOutputStream을 사용합니다.
이 챕터에서는 가장 빈번하게 사용되는 FileInputStream과 FileOutputStream을 살펴보겠습니다.
FileInputStream
터미널에 아래 명령어를 입력하면, code라는 문자열이 입력된 java.txt라는 파일을 생성합니다.
(실습할 코드와 같은 디렉토리에서 입력해야 합니다.)
echo code >> java.txt
아래 코드를 직접 실행하여 출력 결과를 확인해 보세요.
import java.io.FileInputStream;
public class FileInputStreamExample {
public static void main(String args[])
{
try {
FileInputStream fileInput = new FileInputStream("java.txt");
int i = 0;
while ((i = fileInput.read()) != -1) { //fileInput.read()의 리턴값을 i에 저장한 후, 값이 -1인지 확인합니다.
System.out.print((char)i);
}
fileInput.close();
}
catch (Exception e) {
System.out.println(e);
}
}
}
BufferedInputStream이라는 보조 스트림을 사용하면 성능이 향상되기 때문에, 대부분은 이를 사용합니다. 버퍼란 바이트 배열로서, 여러 바이트를 저장하여 한 번에 많은 양의 데이터를 입출력할 수 있도록 도와주는 임시 저장 공간이라고 이해하면 됩니다.
import java.io.FileInputStream;
import java.io.BufferedInputStream;
public class FileInputStreamExample {
public static void main(String args[])
{
try {
FileInputStream fileInput = new FileInputStream("java.txt");
BufferedInputStream bufferedInput = new BufferedInputStream(fileInput);
int i = 0;
while ((i = bufferedInput.read()) != -1) {
System.out.print((char)i);
}
fileInput.close();
}
catch (Exception e) {
System.out.println(e);
}
}
}
보조 스트림도 스트림의 하위 클래스이기 때문에 입출력 방법은 같습니다.
FileOutputStream
import java.io.FileOutputStream;
public class FileOutputStreamExample {
public static void main(String args[]) {
try {
FileOutputStream fileOutput = new FileOutputStream("java.txt");
String word = "ja";
byte b[] = word.getBytes();
fileOutput.write(b);
fileOutput.close();
}
catch (Exception e) {
System.out.println(e);
}
}
}
위 코드를 실행하면, 같은 디렉토리 내에 ja라는 문자열이 입력된 java.txt 파일이 생성됨을 확인할 수 있습니다.
FileReader / FileWriter
앞서 살펴본 File 입출력 스트림은, 바이트 기반 스트림입니다. 바이트 기반은 입출력 단위가 1byte라는 뜻입니다. 하지만 Java에서 char 타입은 2byte(자바 기본 유닛 참고)입니다. 이를 해소하기 위해 자바에서는 문자 기반 스트림을 제공합니다. 문자 기반 스트림에는 FileReader와 FileWriter가 있습니다.
문자 기반 스트림은 문자 데이터를 다룰 때 사용합니다.
문자 기반 스트림과 그 하위 클래스는 여러 종류의 인코딩(encoding)과 자바에서 사용하는 유니코드(UTF-16) 간의 변환을 자동으로 처리합니다.
문자 기반 스트림에서는 일반적으로, 바이트 기반 스트림의 InputStream이 Reader로, OutputStream이 Writer로 대응됩니다. FileInputStream이 문자 기반에서는 FileReader에, FileOutputStream은 FileWriter에 해당하는 식입니다.
FileReader는 인코딩을 유니코드로 변환하고, FileWriter는 유니코드를 인코딩으로 변환합니다.
다음의 예제를 통해 좀 더 알아보겠습니다.
FileReader
public class FileReaderExample {
public static void main(String args[]) {
try {
String fileName = "java.txt";
FileReader file = new FileReader(fileName);
int data = 0;
while((data=file.read()) != -1) {
System.out.print((char)data);
}
file.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
java.txt에 한글을 입력해 보세요. (파일을 직접 수정하면 됩니다.)
그리고 FileReader를 FileInputStream으로 변경해 출력해 봅니다. 무슨 일이 일어나나요?
바이트 기반 스트림과 마찬가지로, Reader에도 성능을 개선할 수 있는 BufferedReader가 있습니다.
public class BufferedReaderExample {
public static void main(String args[]) {
try {
String fileName = "java.txt";
FileReader file = new FileReader(fileName);
BufferedReader buffered = new BufferedReader(file);
int data = 0;
while((data=buffered.read()) != -1) {
System.out.print((char)data);
}
file.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
FileWriter
앞서 만든 java.txt파일에 “written!”이라는 문자열을 입력하는 예제입니다.
public class FileWriterExample {
public static void main(String args[]) {
try {
String fileName = "java.txt";
FileWriter writer = new FileWriter(fileName);
String str = "written!";
writer.write(str);
writer.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
File
앞서 파일을 읽고 쓰는 방법을 간단하게 살펴보았습니다. 이번에는 File 클래스를 알아보겠습니다.
자바에서는 File 클래스로 파일과 디렉토리에 접근할 수 있습니다.
예제를 살펴보겠습니다.
import java.io.*;
public class FileExample {
public static void main(String args[]) throws IOException {
File file = new File("../spring.txt");
System.out.println(file.getPath());
System.out.println(file.getParent());
System.out.println(file.getCanonicalPath());
System.out.println(file.canWrite());
}
}
출력값을 직접 확인해 보고, 각 메서드의 리턴값이 어떤 의미를 갖는지 공식문서를 통해 확인해 보세요. 앞서 만들어놓은 spring.txt 파일이 존재하지 않더라도, 컴파일 에러가 발생하지는 않습니다.
파일 인스턴스를 생성하는 것이 곧 파일을 생성하는 것은 아닙니다. 파일을 생성하기 위해서는 파일 인스턴스를 생성할 때 다음과 같이 첫 번째 인자에 경로를, 두 번째 인자에 파일명을 작성하고, createNewFile() 메서드를 호출해주어야 합니다.
File file = new File("./", "newSpring.txt");
file.createNewFile();
다음은 현재 디렉토리(.)에서 확장자가 .txt인 파일만을 대상으로, 파일명 앞에 “code”라는 문자열을 붙여주는 예제입니다.
import java.io.File;
public class FileClassExample {
public static void main(String[] args) {
File parentDir = new File("./");
File[] list = parentDir.listFiles();
String prefix = "code";
for(int i =0; i <list.length; i++) {
String fileName = list[i].getName();
if(fileName.endsWith("txt") && !fileName.startsWith("code")) {
list[i].renameTo(new File(parentDir, prefix + fileName));
}
}
}
}
스레드(Thread)
지금 여러분이 학습을 위해 사용하고 있는 인터넷 브라우저(Chrome, Safari 등)는 일종의 애플리케이션입니다.
유어클래스로 학습을 진행하고 있는 지금, 이 상태에서, Windows를 사용하신다면 ‘작업 관리자’를, Mac을 사용하신다면 ‘활성 상태 보기’를 열어봅시다. 작업 관리자 및 활성 상태 보기를 열어보면 실행 중인 애플리케이션들의 목록이 쭉 나열됩니다.
위 목록은 어떤 프로그램이 실행 중인지, 그리고 각 프로그램이 컴퓨터의 자원을 얼마나 사용하고 있는지 등을 나타내줍니다.
목록 중, 여러분이 사용하고 있는 애플리케이션들의 이름이 ‘프로세스 이름’이라는 열에 나열되어 있습니다. 즉, ‘프로세스 이름' 열은 현재 실행 중인 애플리케이션들의 이름을 보여줍니다.
어떤 애플리케이션이 실행되면 운영체제가 해당 애플리케이션에 메모리를 할당해 주며 애플리케이션이 실행되는데, 이처럼 실행 중인 애플리케이션을 프로세스라고 합니다. 그리고, 프로세스 내에서 실행되는 소스 코드의 실행 흐름을 스레드라고 합니다.
위 활성 상태 보기 창에서, Chrome은 33개의 스레드를, Discord는 단 한 개의 스레드만을 가지고 있습니다. 이처럼 어떤 프로세스는 단 하나의 스레드만을 가질 수도 있고, 여러 개의 스레드를 가질 수 있습니다.
이때, 단 하나의 스레드를 가지는 프로세스를 싱글 스레드 프로세스, 여러 개의 스레드를 가지는 프로세스를 멀티 스레드 프로세스라고 합니다.
어떤 프로세스가 멀티 스레드로 동작한다는 것은 해당 애플리케이션이 동시 작업을 할 수 있다는 것을 의미합니다. 즉, 여러 코드를 각 스레드에 분배하여 동시에 실행시킬 수 있는 것이죠.
예를 들어, 일반적인 메신저 애플리케이션은 멀티 스레드로 동작하며, 메시지를 주고받으며 동시에 파일을 업로드할 수 있습니다. 만약, 메신저 애플리케이션이 싱글 스레드로 이루어져 있다면, 파일을 업로드하는 동안에는 메시지를 주고받을 수 없을 것입니다.
지금까지 여러분이 봐왔던 모든 예제 코드와 실습 과제들은 싱글 스레드로 코드가 작성되어 있습니다. 소스 코드 내에서 따로 스레드를 생성하지 않았기 때문입니다.
이번 유닛에서는 스레드를 생성하고, 실행하는 등 자바로 스레드를 다루는 방법을 학습합니다. 이번 유닛을 학습하고 나면 여러분들은 멀티 스레드를 활용할 수 있습니다.
멀티 스레드는 다수의 클라이언트 요청을 처리하는 서버를 개발할 때 사용됩니다. 이어지는 콘텐츠에서부터 스레드가 무엇이고, 어떻게 사용할 수 있는지 차근차근 학습을 시작해 봅시다.
학습 목표
- 스레드가 무엇인지 설명할 수 있다.
- 싱글 스레드와 멀티 스레드의 차이를 설명할 수 있다.
- 스레드를 생성하는 두 가지 방법을 활용할 수 있다.
- 스레드를 실행시킬 수 있다.
- 스레드를 동기화할 수 있다.
- 스레드의 상태를 이해하고, 제어할 수 있다.
스레드란?
프로세스(Process)와 스레드(Thread)
앞서 개요에서 언급한 것처럼, 프로세스는 실행 중인 애플리케이션을 의미합니다. 즉, 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 만큼의 메모리를 할당받아 프로세스가 됩니다.
프로세스는 데이터, 컴퓨터 자원, 그리고 스레드로 구성되는데, 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스 코드를 실행합니다. 즉, 스레드는 하나의 코드 실행 흐름이라고 볼 수 있습니다.
메인 스레드(Main thread)
자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드는 main 메서드이며, 메인 스레드가 main 메서드를 실행시켜 줍니다. 메인 스레드는 main 메서드의 코드를 처음부터 끝까지 차례대로 실행시키며, 코드의 끝을 만나거나 return문을 만나면 실행을 종료합니다.
만약, 어떤 자바 애플리케이션의 소스 코드가 싱글 스레드로 작성되었다면, 그 애플리케이션이 실행되어 프로세스가 될 때 오로지 메인 스레드만 가지는 싱글 스레드 프로세스가 될 것입니다. 반면, 메인 스레드에서 또 다른 스레드를 생성하여 실행시킨다면 해당 애플리케이션은 멀티 스레드로 동작하게 됩니다.
멀티 스레드(Multi-Thread)
하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 이를 멀티 스레드 프로세스라 한다고 했습니다. 여러 개의 스레드를 가진다는 것은 여러 스레드가 동시에 작업을 수행할 수 있음을 의미하며, 이를 멀티 스레딩이라고 합니다.
멀티 스레딩은 하나의 애플리케이션 내에서 여러 작업을 동시에 수행하는 멀티 태스킹을 구현하는 데에 핵심적인 역할을 수행합니다.
예를 들어, 여러분들은 메신저 프로그램을 사용할 때, 상대방에게 보낼 사진을 업로드하면서 동시에 메시지를 주고받을 수 있습니다. 이처럼 메신저 프로그램이 여러 가지 작업을 동시에 수행하려면, 작업을 동시에 실행해 줄 스레드가 추가로 필요합니다.
여기까지 스레드가 무엇인지에 대해 살펴보았으니, 다음 콘텐츠에서부터는 스레드를 어떻게 활용할 수 있는지 학습해 봅시다.
스레드의 생성과 실행
이번 콘텐츠에서는 메인 스레드 외의 별도의 작업 스레드를 생성하고 실행하는 방법을 학습합니다.
작업 스레드 생성과 실행
메인 스레드 외에 별도의 작업 스레드를 활용한다는 것은, 다시 말해 작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생성하여 실행시키는 것을 의미합니다.
그런데 자바는 객체지향 언어이므로 모든 자바 코드는 클래스 안에 작성됩니다. 따라서 스레드가 수행할 코드도 클래스 내부에 작성해주어야 하며, run()이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 규정되어 있습니다.
run() 메서드는 Runnable 인터페이스와 Thread 클래스에 정의되어 있습니다. 따라서, 작업 스레드를 생성하고 실행하는 방법은 다음의 두 가지가 됩니다.
- 첫 번째 방법
- Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
- 두 번째 방법
- Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
글로만 봐서는 감이 잘 잡히지 않을 겁니다. 아래에서부터, 직접 인텔리제이에 코드를 입력해 가며 내용을 이해해 보시길 강력히 권장합니다.
1. Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
먼저, Runnable 인터페이스를 구현한 객체를 만들어봅시다. 아래와 같이 임의의 클래스를 만들고, Runnable을 구현하도록 합니다. Runnable에는 run()이 정의되어 있으므로 반드시 run()을 구현해야 합니다.
public class ThreadExample1 {
public static void main(String[] args) {
}
}
// Runnable 인터페이스를 구현하는 클래스
class ThreadTask1 implements Runnable {
public void run() {
}
}
그다음, run()의 메서드 바디에 새롭게 생성된 작업 스레드가 수행할 코드를 적어주면 됩니다. 아래와 같이 코드를 작성해 봅시다.
public class ThreadExample1 {
public static void main(String[] args) {
}
}
class ThreadTask1 implements Runnable {
// run() 메서드 바디에 스레드가 수행할 작업 내용 작성
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
이제 스레드를 생성해 봅시다. Runnable 인터페이스를 구현한 객체를 활용하여 스레드를 생성할 때는 아래와 같이 Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화합니다.
public class ThreadExample1 {
public static void main(String[] args) {
// Runnable 인터페이스를 구현한 객체 생성
Runnable task1 = new ThreadTask1();
// Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화 하여 스레드를 생성
Thread thread1 = new Thread(task1);
// 위의 두 줄을 아래와 같이 한 줄로 축약할 수도 있습니다.
// Thread thread1 = new Thread(new ThreadTask1());
}
}
class ThreadTask1 implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
스레드를 생성하는 것만으로는 run() 내부의 코드가 실행되지는 않습니다. run() 메서드 내부의 코드를 실행하려면 start() 메서드를 아래와 같이 호출하여 스레드를 실행시켜주어야 합니다.
public class ThreadExample1 {
public static void main(String[] args) {
Runnable task1 = new ThreadTask1();
Thread thread1 = new Thread(task1);
// 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
thread1.start();
}
}
class ThreadTask1 implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
마지막으로, main 메서드에 아래와 같이 반복문을 추가한 후, 코드를 실행해 봅시다.
public class ThreadExample1 {
public static void main(String[] args) {
Runnable task1 = new ThreadTask1();
Thread thread1 = new Thread(task1);
thread1.start();
// 반복문 추가
for (int i = 0; i < 100; i++) {
System.out.print("@");
}
}
}
class ThreadTask1 implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
출력 결과는 다음과 같으며, 실행 시마다 다를 수 있습니다. 인텔리제이에서 실행하면 한 줄로 쭉 출력되지만, 여러분의 가독성을 위해 문자가 50개 출력될 때마다 개행을 임의로 추가했습니다.
/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=51746:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample1
@@@@@@@@@@@######@@@@@############################
@#########@@@@@@@@@@@@@@@@############@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@##@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@###########################################
Process finished with exit code 0
출력 결과를 해석해봅 시다.
- @는 main 메서드의 반복문에서 출력한 문자입니다. 즉, @는 메인 스레드의 반복문 코드 실행에 의해 출력되었습니다.
- #는 run() 메서드의 반복문에서 출력한 문자입니다. 즉, #는 작업 스레드의 반복문 코드 실행에 의해 출력되었습니다.
- @와 #는 섞여 있습니다. 즉, 메인 스레드와 작업 스레드가 동시에 병렬로 실행되면서 각각 main 메서드와 run() 메서드의 코드를 실행시켰기 때문에 두 가지 문자가 섞여서 출력된 것입니다.
예제를 타이핑해보기 전에는 잘 감이 잡히지 않으셨을 테지만, 막상 배워보니 별것 아니지 않나요? 이제 스레드를 생성하고 실행하는 두 번째 방법을 배워봅시다.
2. Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
이 방법도 위에서 배운 방법과 크게 다르지 않습니다. 바로 코드를 작성하며 이해해 봅시다.
일단, 위에서 작성한 것과 같이 Thread 클래스를 상속받는 하위 클래스를 만들어줍니다. Thread 클래스에는 run() 메서드가 정의되어 있으며, 따라서 run() 메서드를 오버라이딩해주어야 합니다.
public class ThreadExample2 {
public static void main(String[] args) {
}
}
// Thread 클래스를 상속받는 클래스 작성
class ThreadTask2 extends Thread {
public void run() {
}
}
그다음, run() 메서드 바디에 새롭게 생성될 스레드가 수행할 작업을 작성합니다.
public class ThreadExample2 {
public static void main(String[] args) {
}
}
class ThreadTask2 extends Thread {
// run() 메서드 바디에 스레드가 수행할 작업 내용 작성
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
이제 run() 내의 코드를 실행할 스레드를 생성해 봅시다. 첫 번째 방법과의 차이점은, 이전에 학습한 첫 번째 방법과 달리 Thread 클래스를 직접 인스턴스화하지 않는다는 점입니다.
public class ThreadExample2 {
public static void main(String[]args) {
// Thread 클래스를 상속받은 클래스를 인스턴스화하여 스레드를 생성
ThreadTask2 thread2 = new ThreadTask2();
}
}
class ThreadTask2 extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
마지막으로, 첫 번째 방법에서 실습했던 것과 동일하게 start() 메서드를 실행시켜 주고, main 메서드에 반복문 코드를 추가한 후, 코드를 실행해 봅시다.
public class ThreadExample2 {
public static void main(String[] args) {
ThreadTask2 thread2 = new ThreadTask2();
// 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
thread2.start();
// 반복문 추가
for (int i = 0; i < 100; i++) {
System.out.print("@");
}
}
}
class ThreadTask2 extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
}
두 번째 방법을 사용한 코드를 실행해 보면 첫 번째 방법을 사용한 것과 유사한 결과를 얻을 수 있습니다.
즉, 두 가지 방법은 모두 작업 스레드를 만들고, run() 메서드에 작성된 코드를 처리하는 동일한 내부 동작을 수행합니다.
익명 객체를 사용하여 스레드 생성하고 실행하기
앞서, 스레드가 수행할 동작은 run() 메서드의 바디에 작성해야 하며, 자바는 객체지향 언어이므로 클래스 안에 코드를 작성해야 한다고 했습니다. 이에 따라, ThreadTask1, ThreadTask2를 만들어 그 안에 run() 메서드를 정의했습니다.
그러나, 꼭 이렇게 클래스를 따로 정의하지 않고도 익명 객체를 활용하여 스레드를 생성하고 실행시킬 수 있습니다. 앞서 익명 객체를 잘 학습하셨다면 충분히 이해할 수 있는 내용이니, 아래 예제를 천천히 살펴보며 스스로 이해해 보세요.
Runnable 익명 구현 객체를 활용한 스레드 생성 및 실행
public class ThreadExample1 {
public static void main(String[] args) {
// 익명 Runnable 구현 객체를 활용하여 스레드 생성
Thread thread1 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
});
thread1.start();
for (int i = 0; i < 100; i++) {
System.out.print("@");
}
}
}
Thread 익명 하위 객체를 활용한 스레드 생성 및 실행
public class ThreadExample2 {
public static void main(String[] args) {
// 익명 Thread 하위 객체를 활용한 스레드 생성
Thread thread2 = new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
};
thread2.start();
for (int i = 0; i < 100; i++) {
System.out.print("@");
}
}
}
스레드의 이름
메인스레드는 “main”이라는 이름을 가지며, 그 외에 추가로 생성한 스레드는 기본적으로 “Thread-n”이라는 이름을 가집니다.
이번 콘텐츠에서는 스레드의 이름을 조회하고, 설정하는 방법을 간단하게 학습합니다. 크게 중요한 내용은 아니지만, 이어지는 콘텐츠에 등장하는 예제 코드를 실습하면서 오류가 발생했을 때 적절하게 활용해 보세요.
스레드의 이름 조회하기
스레드의 이름은 아래와 같이 스레드의_참조값.getName()으로 조회할 수 있습니다.
public class ThreadExample3 {
public static void main(String[] args) {
Thread thread3 = new Thread(new Runnable() {
public void run() {
System.out.println("Get Thread Name");
}
});
thread3.start();
System.out.println("thread3.getName() = " + thread3.getName());
}
}
출력 결과
/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52285:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample3
Get Thread Name
thread3.getName() = Thread-0
Process finished with exit code 0
스레드의 이름 설정하기
스레드의 이름은 스레드의_참조값.setName()으로 설정할 수 있습니다.
public class ThreadExample4 {
public static void main(String[] args) {
Thread thread4 = new Thread(new Runnable() {
public void run() {
System.out.println("Set And Get Thread Name");
}
});
thread4.start();
System.out.println("thread4.getName() = " + thread4.getName());
thread4.setName("Code States");
System.out.println("thread4.getName() = " + thread4.getName());
}
}
출력 결과
/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52282:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample4
Set And Get Thread Name
thread4.getName() = Thread-0
thread4.getName() = Code States
Process finished with exit code 0
스레드 인스턴스의 주소값 얻기
스레드의 이름을 조회하고 설정하는 위 두 메서드는 모두 Thread 클래스로부터 인스턴스화된 인스턴스의 메서드이므로, 호출할 때 스레드 객체의 참조가 필요합니다.
만약, 실행 중인 스레드의 주소값을 사용해야 하는 상황이 발생한다면 Thread 클래스의 정적 메서드인 currentThread()를 사용하면 됩니다.
public class ThreadExample1 {
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
thread1.start();
System.out.println(Thread.currentThread().getName());
}
}
출력 결과
/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52293:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample1
main
Thread-0
Process finished with exit code 0
스레드의 동기화
스레드 동기화란?
앞서 프로세스는 자원, 데이터, 그리고 스레드로 구성된다고 했습니다. 프로세스는 스레드가 운영 체제로부터 자원을 할당받아 소스 코드를 실행하여 데이터를 처리합니다.
이때, 싱글 스레드 프로세스는 데이터에 단 하나의 스레드만 접근하므로, 문제 될 사항이 없습니다.
그러나, 멀티 스레드 프로세스의 경우, 두 스레드가 같은 데이터를 공유하게 되어 문제가 발생할 수 있습니다.
예제를 통해 구체적으로 어떤 문제가 발생하는지 확인해 봅시다. 예제 코드를 보기 전에 먼저 여러분들에게 생소할 만한 문법 요소를 간단하게 설명하겠습니다.
- try { Thread.sleep(1000); } catch (Exception error) {}
- Thread.sleep(1000);
- 스레드를 일시 정지시키는 메서드입니다. 이에 대해서는 이어지는 콘텐츠 \[스레드의 상태와 상태 제어]에서 학습합니다. 참고로, 어떤 스레드가 일시 정지되면, 대기열에서 기다리고 있던 다른 스레드가 실행됩니다.
- 또한, Thread.sleep()은 반드시 try … catch문의 try 블록 내에 작성해주어야 합니다.
- 여기에서는 간단하게, 스레드의 동작을 1초 동안 멈추는 코드라고 이해해 주세요.
- try { … } catch ( ~ ) { … }
- try … catch문은 예외 처리에 사용되는 문법입니다.
- 쉽게 설명하자면, try의 블록 내의 코드를 실행하다가 예외 또는 에러가 발생하면 catch문의 블록에 해당하는 내용을 실행하라는 의미가 됩니다.
- Thread.sleep(1000);의 동작을 위해 형식적으로 사용한 문법 요소이니, 여기에서는 큰 의미를 두지 않고 그냥 넘어가셔도 됩니다.
- Thread.sleep(1000);
예제 코드 이해해 필요한 내용들을 설명했으니, 이제 아래 예제 코드를 직접 입력해 보세요.
public class ThreadExample3 {
public static void main(String[] args) {
Runnable threadTask3 = new ThreadTask3();
Thread thread3_1 = new Thread(threadTask3);
Thread thread3_2 = new Thread(threadTask3);
thread3_1.setName("김코딩");
thread3_2.setName("박자바");
thread3_1.start();
thread3_2.start();
}
}
class Account {
// 잔액을 나타내는 변수
private int balance = 1000;
public int getBalance() {
return balance;
}
// 인출 성공 시 true, 실패 시 false 반환
public boolean withdraw(int money) {
// 인출 가능 여부 판단 : 잔액이 인출하고자 하는 금액보다 같거나 많아야 합니다.
if (balance >= money) {
// if문의 실행부에 진입하자마자 해당 스레드를 일시 정지 시키고,
// 다른 스레드에게 제어권을 강제로 넘깁니다.
// 일부러 문제 상황을 발생시키기 위해 추가한 코드입니다.
try { Thread.sleep(1000); } catch (Exception error) {}
// 잔액에서 인출금을 깎아 새로운 잔액을 기록합니다.
balance -= money;
return true;
}
return false;
}
}
class ThreadTask3 implements Runnable {
Account account = new Account();
public void run() {
while (account.getBalance() > 0) {
// 100 ~ 300원의 인출금을 랜덤으로 정합니다.
int money = (int)(Math.random() * 3 + 1) * 100;
// withdraw를 실행시키는 동시에 인출 성공 여부를 변수에 할당합니다.
boolean denied = !account.withdraw(money);
// 인출 결과 확인
// 만약, withraw가 false를 리턴하였다면, 즉 인출에 실패했다면,
// 해당 내역에 -> DENIED를 출력합니다.
System.out.println(String.format("Withdraw %d₩ By %s. Balance : %d %s",
money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
);
}
}
}
위 예제 코드는 두 개의 작업 스레드를 생성하여 1,000원의 잔액을 가진 계좌로부터 100~300원을 인출하고, 인출금과 잔액을 출력하는 예제입니다.
위 코드를 실행하면 두 개의 작업 스레드가 생성되며, 이 작업 스레드는 Account 객체를 공유하게 됩니다.
출력 결과를 확인해 봅시다. 출력 결과는 실행 시마다 다를 수 있습니다.
/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52484:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample3
Withdraw 100₩ By 김코딩. Balance : 600
Withdraw 300₩ By 박자바. Balance : 600
Withdraw 200₩ By 김코딩. Balance : 400
Withdraw 200₩ By 박자바. Balance : 200
Withdraw 200₩ By 김코딩. Balance : -100
Withdraw 100₩ By 박자바. Balance : -100
Process finished with exit code 0
출력 결과를 보면, 일단 인출금과 잔액이 제대로 출력되지 못하고 있습니다. 출력 결과 첫째 줄에서, 김코딩에 의해 100원이 인출되었는데 잔액이 900원이 아니라, 600원이라고 출력되고 있습니다.
이는 두 스레드 간에 객체가 공유되기 때문에 발생하는 오류로, 값이 왜 이렇게 출력되었는지 추측하기가 어렵습니다.
또한, withdraw()에서 잔액이 인출하고자 하는 금액보다 많은 경우에만 인출이 가능하도록 코드를 작성해 두었음에도 불구하고(if (balance >= money) ~), 마치 조건문이 무시된 것처럼 음수의 잔액이 발생하는 것을 확인할 수 있습니다.
여기에서 음수의 잔액이 발생하는 이유는 간단합니다. 두 스레드가 하나의 Account 객체를 공유하는 상황에서, 한 스레드가 if 문의 조건식을 true로 평가하여 if문의 실행부로 코드의 흐름이 이동하는 시점에 다른 스레드가 끼어들어 balance를 인출했기 때문입니다.
그리고, 알 수 없는 원인에 의해 인출에 실패한 경우에 -> DENIED가 제대로 출력되지 않는 문제도 발생하고 있습니다.
이제 이러한 상황이 발생하지 않게 하는 것을 바로 스레드 동기화라고 합니다. 스레드 동기화를 적용하면 위의 예제에서 발생하는 문제를 해결할 수 있습니다.
스레드 동기화 적용하여 위의 코드 예제를 개선하기에 앞서, 먼저 임계영역과 **락(Lock)**에 대한 이해가 필요합니다.
임계 영역(Critical section)과 락(Lock)
임계 영역은 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역을 의미하며, 락은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미합니다.
즉, 임계 영역으로 설정된 객체가 다른 스레드에 의해 작업이 이루어지고 있지 않을 때, 임의의 스레드 A는 해당 객체에 대한 락을 획득하여 임계 영역 내의 코드를 실행할 수 있습니다.
이때, 스레드 A가 임계 영역 내의 코드를 실행 중일 때에는 다른 스레드들은 락이 없으므로 이 객체의 임계 영역 내의 코드를 실행할 수 없습니다.
잠시 뒤 스레드 A가 임계 영역 내의 코드를 모두 실행하면 락을 반납합니다. 이때부터는 다른 스레드 중 하나가 락을 획득하여 임계 영역 내의 코드를 실행할 수 있습니다.
위의 예제에서 우리에게 필요했던 것은 두 스레드가 동시에 실행하면 안 되는 영역을 설정하는 것입니다. 즉, withraw() 메서드를 두 스레드가 동시에 실행하지 못하게 해야 합니다.
이를 임계 영역과 락이라는 용어를 사용하여 표현하면 다음과 같습니다.
withdraw() 메서드를 임계 영역으로 설정해야 합니다.
특정 코드 구간을 임계 영역으로 설정할 때는 synchronized라는 키워드를 사용합니다. synchronized 키워드는 두 가지 방법으로 사용할 수 있습니다.
1. 메서드 전체를 임계 영역으로 지정하기
아래와 같이 메서드의 반환 타입 좌측에 synchronized 키워드를 작성하면 메서드 전체를 임계 영역으로 설정할 수 있습니다. 이렇게 메서드 전체를 임계 영역으로 지정하면 메서드가 호출되었을 때, 메서드를 실행할 스레드는 메서드가 포함된 객체의 락을 얻습니다.
즉, withdraw()가 호출되면, withdraw()를 실행하는 스레드는 withdraw()가 포함된 객체의 락을 얻으며, 해당 스레드가 락을 반납하기 이전에 다른 스레드는 해당 메서드의 코드를 실행하지 못하게 됩니다.
class Account {
...
public synchronized boolean withdraw(int money) {
if (balance >= money) {
try { Thread.sleep(1000); } catch (Exception error) {}
balance -= money;
return true;
}
return false;
}
}
2. 특정한 영역을 임계 영역으로 지정하기
특정 영역을 임계 영역으로 지정하려면 아래와 같이 synchronized 키워드와 함께 소괄호(()) 안에 해당 영역이 포함된 객체의 참조를 넣고, 중괄호({})로 블록을 열어, 블록 내에 코드를 작성하면 됩니다.
이 경우에도 마찬가지로, 임계 영역으로 설정한 블록의 코드로 코드 실행 흐름이 진입할 때, 해당 코드를 실행하고 있는 스레드가 this에 해당하는 객체의 락을 얻고, 배타적으로 임계 영역 내의 코드를 실행합니다.
class Account {
...
public boolean withdraw(int money) {
synchronized (this) {
if (balance >= money) {
try { Thread.sleep(1000); } catch (Exception error) {}
balance -= money;
return true;
}
return false;
}
}
}
자, 이제 드디어 문제가 발생했던 예제 코드를 개선해 볼 차례입니다.
그런데, 싱겁게도 크게 개선할 부분이 없습니다. 단지 바로 위의 예제에서 학습했던 두 가지 방법 중 하나를 선택해서 적용하면 됩니다.
여기에서는 메서드 전체를 임계 영역으로 설정하는 방법을 사용해 보겠습니다.
아래와 같이 withdraw()의 반환 타입 앞에 synchronized를 붙여 준 후, 코드를 실행하고 결과를 확인해 보세요.
public class ThreadExample3 {
public static void main(String[]args) {
Runnable threadTask3 = new ThreadTask3();
Thread thread3_1 = new Thread(threadTask3);
Thread thread3_2 = new Thread(threadTask3);
thread3_1.setName("김코딩");
thread3_2.setName("박자바");
thread3_1.start();
thread3_2.start();
}
}
class Account {
private int balance = 1000;
public int getBalance() {
return balance;
}
public synchronized boolean withdraw(int money) {
if (balance >= money) {
try {
Thread.sleep(1000);
} catch (Exception error) {
}
balance -= money;
return true;
}
return false;
}
}
class ThreadTask3 implements Runnable {
Account account = new Account();
public void run() {
while (account.getBalance() > 0) {
int money = (int)(Math.random() * 3 + 1) * 100;
boolean denied = !account.withdraw(money);
System.out.println(String.format("Withdraw %d₩ By %s. Balance : %d %s",
money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
);
}
}
}
출력 결과는 다음과 같습니다. 출력 결과는 실행 시마다 다를 수 있습니다.
/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52534:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample3
Withdraw 100₩ By 김코딩. Balance : 900
Withdraw 100₩ By 박자바. Balance : 800
Withdraw 200₩ By 김코딩. Balance : 600
Withdraw 300₩ By 박자바. Balance : 300
Withdraw 300₩ By 김코딩. Balance : 0
Withdraw 100₩ By 박자바. Balance : 0 -> DENIED
Process finished with exit code 0
이제 인출금에 따라 올바르게 잔액이 출력되며, 잔액이 음수인 경우에도 올바른 값이 출력됩니다.