백엔드 데브코스 TIL 4기 - 06.07(수)

신재윤·2023년 6월 7일
0
post-thumbnail

💻 2주차 DAY 03

2주차 DAY 03 ~! 강의를 2일치 몰아들었다. 모던 자바의 컬렉션과 스트림에 관한 이야기와 본격적인 객체지향 설계를 학습했다. 특히, 함수형 인터페이스를 적용하는 부분에서 곰튀김님께서 설명을 너무너무 잘해주셔서 아하 모먼트가 많았고 수업을 재밌게 들었다. 마지막으로, 본격적인 계산기 과제를 위하여 간단하게 설계해보는 시간을 가졌다.


Collection

사실 JAVA가 약한 나는 컬렉션이 친숙하지 않았다. 단순히 자료구조라고만 생각하고 있었는데, 조금 더 이해해보면 여러 데이터의 묶음이 컬렉션이라고 이해할 수 있다.

  • 데이터 덩어리를 받아서, 데이터 덩어리를 내보내자.
  • 컬렉션은 추상체-구상체의 구조를 띈다.
    • Collection (추상체)
      • List (Colleciton의 구상체)
        • LinkedList (List의 구상체)
        • ArrayList (List의 구상체)
        • Vector (List의 구상체)
        • Stack (List의 구상체)
      • Set
        • HashSet

함수형 인터페이스 적용

이전 시간에 JAVA API에서 제공해주는 함수형 인터페이스에 관하여 배우기는 하였지만, 어떤식으로 적용되는지 사실 와닿지 않았었다. 오늘 컬렉션에 적용하는 모습을 보면서 살~짝 느낌을 잡았다.

  • 함수형 인터페이스를 활용하여 고정된 형태의 기능을 가지는 것이 아니라, 사용하는 측에서 그 기능을 정의할 수 있는 범용적인 형태의 컬렉션을 만들어 냄
  • 이렇게 여러 데이터를 묶어서 다루는 것을 Collection이라고 한다. Collection은 데이터의 묶음이다. filter를 하든 map을 하든 묶여있는 데이터 덩어리를 내보냄

예제 1. forEach 만들기 + consumer 적용

public void foreach() {
	for (int i = 0; i < list.size(); i++) {
    	T data = list.get(i);
    }
}
  • foreach() 메서드는 data에서 무언가를 하려고 하는데, return 없이 void인 상태이다. 함수형 인터페이스의 consumer !?
public void foreach(Consumer<T> consumer) {  
	for (int i = 0; i < list.size(); i++) {  
		T data = list.get(i);  
		consumer.accept(data);  
	}  
}
  • consumer를 사용해서 accept로 받아주는 모습이다.
public class Main {
	public static void main(String[] args) {
		// 익명 클래스
		new MyCollection<>(Arrays.asList(1, 2, 3, 4, 5))  
			.foreach(new Consumer<Integer>() {  
				@Override  
				public void accept(Integer integer) {  
					System.out.println(integer);  
				}  
			});

		// 람다식
		new MyCollection<>(Arrays.asList(1, 2, 3, 4, 5, "A"))
			.foreach(System.out::println);
	}
}
  • 익명 클래스 사용과 람다식의 사용이 익숙하지 않아서 만들어본 foreach() 메서드를 사용하는 예제도 넣어봤다.

예제 2. 데이터 변형해보기 + function 적용

public <U> MyCollection<U> map(Function<T, U> function) {  
	List<U> newList = new ArrayList<>();  
	foreach(data -> newList.add(function.apply(data)));  
	return new MyCollection<>(newList);  
}
  • 데이터를 변경하는 것을 매핑한다 혹은 map 과 같이 표현함
  • public MyCollection<U> map(Function<T, U> function) 이라고 하면 오류 발생
    • 1번에서 정의한 MyCollection에는 <T>만 있음
    • 이 map 메서드에서만 사용하는 <U> 타입에 대하여 추가해야함
    • 그래서 public <U> MyCollection<U> map(Function<T, U> function) 형태가 만들어짐
  • newList에는 List에 들어있는 T 타입의 값이 function이라는 함수형 인터페이스에 의해 새로운 U 타입으로 바뀐 결과가 들어감

예제 3. 필터 만들기 + Predicate 적용

public MyCollection<T> filter(Predicate<T> predicate) {  
	List<T> newList = new ArrayList<>();  
	foreach(data -> {  
		if (predicate.test(data)) newList.add(data);  
	});  
	return new MyCollection<>(newList);  
}
  • 파라미터가 있고 그에 대한 boolean 값을 반환해주는 predicate 사용
  • 결과가 참이면 newList에 추가되도록~

Iterator

Colleection은 데이터 덩어리를 받아서, 데이터 덩어리를 내보낸다고 하였다. 이러한 여러 데이터의 묶음을 풀어서 하나씩 처리하고 싶은 경우가 있는데, 이러한 수단을 제공하는 것이 Iterator이다. next()를 통해 순방향으로 데이터를 조회하는 것은 가능하지만, 역순으로 움직일 수는 없다.

정리해보면 Iterator는 데이터 덩어리인 Collection을 풀어서 하나씩 사용하는 형태라는 것이다. 하지만, 이렇게 되면서 데이터 묶음으로 처리하는 Collection에서 사용하던 편리한 기능인 고차함수 map, filter, reduce 같은 기능을 사용하지 못하게 되었다.

그러면, Iterator 같이 하나씩 풀어쓰는 형식에서는 함수형 인터페이스를 활용한 고차함수를 사용할 수 없을까?


Stream

그래서 등장한 것이 Stream이다. Stream은 데이터의 연속으로, 연속된 데이터 중 하나를 의미한다. 내가 이야기할 것은 모던 자바의 Stream이다. IO 스트림도 있지만, 그것은 Dev Uni 기록용 블로그 - IO 스트림 을 참고하자.

모던 자바에 추가된 스트림은 데이터 스트림으로, 제공되는 Collections.stream()을 의미한다. 컬렉션 안의 데이터를 연속적인 흐름으로 처리할 수 있게 되었다. 추가로, 고차함수 filter, map, forEach가 제공되어 아주 편하다.

⭐️ Stream의 등장으로 인하여, 연속된 데이터에 대한 풍부한 고차함수들을 사용하여 강력한 기능을 간결하게 표현할 수 있게 되었다 !!


Stream을 생성하는 방식에는 generateiterate가 있다.

  • Stream.generate()
    • generate()의 파라미터로 supplier가 들어감
    • 함수형 인터페이스 supplier는 input 값은 없고 결과값이 무언가 나와야하는 것을 의미한다.
Random r = new Random();  
Stream.generate(() -> r.nextInt())  
	.limit(10)  
	.forEach(System.out::println);

  • Stream.iterate()
    • 초기 값인 시드값이 들어오고, 그 다음에 어떠한 결과를 전달하는 방식
    • 파라미터 2개를 이용한다.
Stream.iterate(0, (i) -> i + 1)  
	.limit(20)  
	.forEach(System.out::println);

Optional

개발자로 살아가며 가장 많이 마주할 문제가 바로 NPE (Null Pointer Exception)이다. Java는 거의 모든 것이 reference value라서 null이 될 가능성을 항상 가지고있다. 그래서 매번 null 체크를 확인해야할 필요가 있었고, 이러한 Optional이 등장하게 되었다.

여담이지만, 예전에 공부했던 바로는 사실 Optional을 사용하지 않는 상황이 제일 좋다고 한다. 즉, null이 나올 것 같다고 무조건 Optional을 때려버리는 것이 아니라 최대한 방어적으로 코딩하는 것이 좋다고 한다.


Optional은 null이 될 수 있는 값들을 운반해주는 일종의 캐리어이다. 즉, Optional이 비어있는지 아닌지를 체크하여 NPE를 피하면서 null인지 아닌지를 알 수 있는 것이다.

  • isEmpty() -> 값이 없으면 true ( = null이면 true )
  • isPresent() -> 값이 있으면 true

숫자야구 프로젝트

실습 프로젝트 수업에서는 숫자야구를 구현해보는 방식이었다. 내가 봐왔던 숫자야구 구현과는 다르게 곰튀김님께서는 함수형 인터페이스를 자유자재로 다루셨고, IDE의 기능을 최대한 활용하여 상황에 맞게 코드를 짜시는 것을 보고 감탄했다.

코드를 이해하기는 했는데, 생각의 흐름이 너무 좋았어서, 이는 강의와 코드를 여러 번 반복해서 보려고 한다. 완벽하게 내 것으로 만들고 나중에 따로 포스팅하는 것을 추가하도록 하겠다.


계산기 프로젝트 preview


프로젝트 세팅 과정

오늘은 프로젝트 세팅과 간단한 설계에서 끝났다. 프로젝트 세팅에 관해서 조금 애먹었는데, 먼저 fork 뜨고 clone 이후 작업 branch를 하나 만들고 .gitignore 추가까지는 평탄했다.

그러나, 프로젝트를 생성하려고 보니 클론한 프로젝트에는 README.md 파일 밖에 없어서 gradle을 추가로 설정해야하는 상태였다. New Project를 하려고 하니 새로운 폴더가 계속 형성되었고 java-calculator/java-calculator 이런식으로 되니까 보기가 불편했다.

그래서, 1주차에 학습했던 것을 토대로 터미널로 프로젝트 폴더에 들어가서 gradle init으로 환경을 구성했다. 기존에 인텔리제이로 new project 하던 것보다 더 많은 파일이 나왔고, build.gradle에도 하나하나 주석이 달려있어서 사실 보기 좀 불편했다.

그러던 와중 데브코스 팀원께서 방법을 찾으셔서 그 방법으로 시도했다.

  1. 클론한 프로젝트 폴더에서 new project로 내부에 프로젝트 그대로 생성
  2. 생성한 프로젝트 파일을 그대로 밖으로 가져와서 복사 붙혀넣기
  3. 인텔리제이 껐다가 키고 gradle 싱크 맞추기

이렇게하니 기존에 인텔리제이로 만들던 것과 동일하여서 드디어 기쁘게 시작할 수 있었다...!! 아래는 gitignore 세팅과 추가적인 IDE 세팅들이다.


  • gitignore
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**
!**/src/test/**

### macOS ###
.DS_Store

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
# Ignore Gradle build output directory
build

# Ignore Gradle GUI config
gradle-app.setting

# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties

# Cache of project
.gradletasknamecache

### Gradle Patch ###
# Java heap dump
*.hprof
  • 인텔리제이 세팅
  1. File → Project Structures → SDK 자바 버전 11 확인
  2. Settings → Build → Build Tools → Gradle → 둘다 IntelliJ Idea로 수정
  3. Settings → Tools → Actions on Save → Optimize imports 체크
  4. Settings → Editor → General → Auto Import → 가운데 2개 import fly 체크
  5. Settings → Editor → Code Style → Java → 숫자 2개 999로 수정

계산기 설계

아직 디테일하게 설계는 못했지만, 최대한 SOLID 원칙을 신경쓰며 객체지향적으로 구성하려고 노력해보고 있는 상태이다. 지금까지 생각해본 구조는 대략 아래와 같다.

  • View
    • Console
  • Model
    • Calculation (인터페이스, 추상체)
      • Plus (구상체)
      • Minus (구상체)
      • Divide (구상체)
      • Multiple (구상체)
    • Priority
  • Repository
    • InMemoryRepository (인터페이스, 추상체)
      • MapMemory (구상체)
  • Controller

음~! 근데 각각 계산 방식에 대하여 저렇게 동사는 메서드니까 저걸 구현하는 것으로 가는게 아니라 연산자와 피연산자 객체를 설정하고 그에 해당하는 계산을 따로 빼야하나 싶기도 하고 ,, 컨트롤러는 어떻게 모델과 뷰를 연결시킬지에 대한 고민도 해봐야 할 것 같다.

profile
백엔드 데브코스 TIL (Today I Learned)

0개의 댓글