인프런 워밍업 클럽 스터디(BE) 0기 / 1주차 발자국

인프런 워밍업 클럽 스터디(BE) 0기 / 1주차 발자국

인프런 워밍업 클럽 스터디(BE) 0기 / 1주차 발자국

image

📕일주일 간의 학습 내용에 대한 간단한 회고

커리큘럼에 따라 매일마다 최소 20분에서 1시간 사이의 짧은 강의를 수강했지만, 매일 최소 3시간 복습하며 각 강의마다 전달하고자 하는 지식을 체득하기 위해 노력하는 중입니다. 최태현 멘토님의 열정적인 강의와 적극적인 피드백에 많은 동기부여가 됐습니다. 남은 커리큘럼도 최선을 다하고 진정성 있게 참여하겠습니다. 🐜

일주일 동안 스스로 칭찬하고 싶은 점 : 신입 채용 서류 광탈에도 최선을 다하는 나에게 칭찬 ^^

아쉬웠던 점 : 순간 놓치는 1초 2초가 어쩌면 평생 놓친 시간이 될 수도 있음에도 불구하고, 병든 닭마냥 꾸벅꾸벅 졸고 있음

보완하고 싶은 점 : 깃허브 프로젝트 칸반과 위키에 학습 내용 정리 방식을 좀 더 직관적으로 보완할 필요성이 있음

다음주에는 어떤 식으로 학습하겠다는 스스로의 목표 : 에빙 하우스의 망각 곡선에 따라 효율적인 학습을 실천

1. 소개

지식공유자 최태현님이 운영하는 인프런 워밍업 클럽 스터디의 학습 내용 정리하는 공간입니다.

인프런 워밍업 스터디 클럽 0기🍃 는[자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! 서버 개발 올인원 패키지] 에서 자세한 학습 내용과 인프런 워밍업 클럽 스터디 진행에 대한 전반적인 소개는 인프런에서 참고하기 바랍니다. Wiki 에서는 매일마다 학습해야 되는 내용들을 정리하고 데일리 과제를 정리하는 공간으로 활용하여 나중에 시작하게 될 프로젝트 참고 자료로 활용합니다.

모든 학습 내용은 깃허브 리포지토리에서 다음과 같이 확인할 수 있습니다. 🍃깃허브로 이동하기

  • 위키 : 미션 및 미니 프로젝트와 정리하는 공간

  • 깃허브 프로젝트 : 각 장마다 학습한 내용을 정리하는 공간

  • 깃허브 전략

    • feature 브랜치와 main 브랜치로 구성하여 강의의 각 장마다 이슈 발행

    • 깃허브 프로젝트의 칸반과 이슈 번호를 연동하고 커밋에 학습 내용을 요약 정리

    • 이슈에서 각 장에 대한 요약 정리 확인이 가능


2. 커리큘럼

1주차 학습 내용 정리는 커밋과 깃허브 프로젝트에서 자세한 내용을 확인할 수 있습니다. 🍃깃허브로 이동하기

  • Day 2 | 2.19(월) | 서버 개발을 위한 환경 설정 및 네트워크 기초강의 (1~5강) | 미션 O

  • Day 3 | 2.20(화) | 첫 HTTP API 개발강의 (6~9강) | 미션 O

  • Day 4 | 2.21(수) | 기본적인 데이터베이스 사용법강의 (10~13강) | 미션 O

  • Day 5 | 2.22(목) | 데이터베이스를 사용해 만드는 API강의 (14~16강) | 미션 O

  • Day 6 | 2.23(금) | 클린코드의 개념과 첫 리팩토링강의 (17~18강) | 미션 O


3. 학습 내용 요약

학습 내용 요약은 해당 강의에서 핵심 내용을 최대한 간단하게 정리하고 핵심 키워드를 작성합니다. 하지만 개인적으로 특정 장에서 자세한 설명이 필요하다고 느껴진다면, 제목에 🌈 표시와 함께 자세한 설명과 함께 해당 장을 정리합니다. 각 장마다 자세한 학습 내용 요약은 🍃깃허브로 이동하기 에서 확인할 수 있습니다.

3.1 서버 개발을 위한 환경 설정 및 네트워크 기초강의 (1~5강)

  • 1강 스프링부트 프로젝트 생성 및 최태현 멘토님의 강의 첨부 자료에 대해 소개

    • https://start.spring.io/, 강의 자료

  • 2강 어노테이션, 서버, 요청에 대한 이해와 스프링부트 초기화를 담당하는 @SpringBootApplication 학습

    • 어노테이션, 서버, 요청, @SpringBootApplication

  • 3강 이세계와 실세계의 사례로 네트워크의 전반적인 흐름에 대한 이해

    • IP, Domain, DNS, Port, HTTP Header & Body, OSI 7 Layer, TCP/IP Layer, 3 Way Handshake

🌈 4강 HTTP와 API 구성과 동작 원리에 대한 이해

4.1 HTTP 메서드

  • 클라이언트와 서버 사이에 이뤄지는 요청과 응답 데이터를 전송하는 방식

  • 쉽게 말하면, 서버에 주어진 리소스에 수행하길 원하는 행동

  • 서버가 수행해야 할 동작을 지정하는 요청을 보내는 방법

HTTP 메소드의 종류는 총 9가지가 있다. 이 중 주로 쓰이는 메소드는 5가지로 보면 된다.

4.2 HTTP 주요 메소드

  1. GET : 리소스 조회(데이터 요청, 쿼리)

  2. POST: 요청 데이터 처리, 주로 등록에 사용(데이터 저장, 바디)

  3. PUT : 리소스를 대체(덮어쓰기), 해당 리소스가 없으면 생성(데이터 수정, 바디)

  4. PATCH : 리소스 부분 변경 (PUT이 전체 변경, PATCH는 일부 변경)

  5. DELETE : 리소스 삭제(데이터 삭제, 쿼리)

4.3 쿼리와 바디는 정보를 보내는 2가지 방법

  1. GET 에서는 쿼리를 사용

  2. POST 에서는 바디를 사용

4.3.1 GET 예제

GET/portion?color=red&color=2
Host:spring.com:3000
  • GET : HTTP Method

  • Host:spring.com:3000 : HTTP 요청을 받는 컴퓨터와 프로그램 정보를 의미

  • /portion : HTTP 요청을 받는 컴퓨터에게 원하는 자원(Path)

  • ? : 구분 기호

  • color=red : 자원의 세부 조건(색 : 빨강)

  • & : 구분 기호(다른 세부 조건과 구분하기 위한 기호)

  • color=2 : 자원의 세부 조건(개수 2개)

  • color=red&color=2 : 쿼리에 해당

4.3.2 POST 예제

POST /oak/leather
Host: spring.com:3000
  • POST : 요청을 받는 컴퓨터에게 저장

  • Host: spring.com:3000 : HTTP 요청을 받는 컴퓨터와 프로그램 정보를 의미

  • /oak/leather : HTTP 요청을 받는 컴퓨터에게 원하는 자원(Path)

  • 자원정보 : 다음 시간에 설명(바디에 해당)

POST /oak/leather의 의미인 행위와 자원은 HTTP 요청을 보내기 전에 약속해야 합니다.

4.4 API

  • API(Application Programming Interface)는 정해진 약속을 하여, 특정 기능을 수행하는 것

    image

    헤더와 바디 사이에 한 칸을 띄우고 작성합니다.

     

    4.5 URL

    URL(Uniform Resource Locator)는 흔히 브라우저 주소 칸에 작성하는 주소를 의미합니다.

    • http : 사용하고 있는 프로토콜

    • :// : 구분 기호

    • spring.com:3000 : 도메인이름:포트(도메인 이름은 IP로 대체 가능)

    • /portion : 자원의 경로(Path)

    • ? : 구분 기호

    • color=red&count=2 : 쿼리(추가 정보)

요약 정리

  1. (웹을 통한) 컴퓨터 간의 통신은 HTTP 라는 표준화된 방식이 존재

  2. HTTP의 요청은 HTTP Method (GET, POST)와 Path(/portion)가 핵심이다.

  3. 요청에서 데이터를 전달하기 위한 2가지 방법은 쿼리바디이다.

  4. HTTP 응답은 상태 코드가 핵심이다.

  5. 클라이언트와 서버는 HTTP를 주고 받으며 기능을 동작하는데 이때 정해진 규칙을 API라고 한다.

🌈 5강 GET API 개발하고 테스트하기

5.1 덧셈 API

이번 시간에는 덧셈 API 를 직접 생성합니다. 두 수의 합 결과를 반환합니다.

  • @RestController : 주어진 ClassController로 등록한다. Controller는 API 입구를 의미한다.

  • @GetMapping("/add) : 아래 함수를 HTTP Method가 GET 이고 HTTP path가 /add인 API로 지정한다.

  • @RequestParam : 주어진 쿼리 함수 파라미터에 넣는다.

5.2 @RequestParam

  • HTTP 요청에서 파라미터를 추출하는 데 사용되는 어노테이션.

  • url의 number1=100&number2=200에 요청 파라미터에 해당

  • 요청 파라미터가 많은 경우에는@RequestParam을 사용 하기 보다는 예제처럼 CalculatorAddRequest 객체를 생성하고 getter 메서드로 요청 파라미터를 핸들링. 이를 dto 라고 한다. (DTO(Data Transfer Object, 데이터 전송 객체)란 프로세스 간에 데이터를 전달하는 객체를 의미)

5.3 @RestController

  • @RestController가 작성된 해당 클래스의 모든 메서드는 HTTP 요청에 응답하는 컨트롤러의 역할을 수행

  • @Controller@ResponseBody가 추가

  • @RestController는 JSON 형태로 객체 데이터를 반환

스프링 MVC에서 컨트롤러 메서드는 일반적으로 뷰에 데이터를 전달하고 해당 뷰를 렌더링하는 데 사용됩니다. 그러나 @ResponseBody를 사용하면 컨트롤러 메서드가 반환하는 객체가 HTTP 응답 본문에 직접 포함되어 클라이언트로 전송됩니다. 이것은 주로 RESTful 웹 서비스를 구축할 때 JSON이나 XML과 같은 데이터 형식으로 데이터를 반환할 때 사용됩니다.

5.4 포스트맨 결과 화면

image

컨트롤러

package com.group.libarayapp.controller.calculator;
​
import com.group.libarayapp.dto.calculator.request.CalculatorAddRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class CalculatorController {
​
    /**
     * http://localhost:8080/add?number1=100&number2=200
     * @apiNote @RequestParam 이란, HTTP 요청에서 파라미터를 추출하는 데 사용되는 어노테이션
     * @param number1 요청 파라미터
     * @param number2 요청 파라미터
     */
    @GetMapping("/add") // GET /add
    public int addTwoNumbers(
            @RequestParam int number1,
            @RequestParam int number2
             ) {
        return number1 + number2;
    }
​
    /**
     * http://localhost:8080/add-request?number1=100&number2=200
     * @param request @RequestParam 을 제거하고 객체를 요청 파라미터로 보낼 수 있다.
     * @return 객체의 getter 메서드로 접근하여 두 필드의 값을 더한다.
     */
    @GetMapping("/add-request") // GET /add
    public int addTwoNumbersRequest(CalculatorAddRequest request) {
        return request.getNumber1() + request.getNumber2();
    }
}

요청 dto

package com.group.libarayapp.dto.calculator.request;
​
public class CalculatorAddRequest {
​
    private final int number1;
    private final int number2;
​
    public CalculatorAddRequest(int number1, int number2) {
        this.number1 = number1;
        this.number2 = number2;
    }
​
    public int getNumber1() {
        return number1;
    }
​
    public int getNumber2() {
        return number2;
    }
}

3.2 첫 HTTP API 개발강의 (6~9강)

🌈 6강 POST API 개발하고 테스트하기

6.1 Q. POST에서는 데이터를 어떻게 받을까? A. HTTP Body 를 이용합니다. 이때 사용되는 문법이 있는데, JSON 이라고 합니다. (JavaScript Object Notation, JSON)

6.2 JSON

  • 객체 표기법,즉 무언가를 표현하기 위한 형식이다!

  • 중괄호 안에 작성한다 {}

  • 키(key)와 값(value)으로 구성한다.

  • Java로 비유하자면, Map<Object, Object> 와 비슷한 느낌에 해당한다. Object 타입의 경우에도 다양한 타입이 가능한 것 처럼, JSON 또한 다양한 타입을 지원하기 때문이다. JSON 안에 JSON 또한 가능하다.

    {
        "name" : "최태현",
        "age" : 99
        “house”: {
       “address”: “대한민국 서울”,
       “hasDoor”: true
       }
    }

6.3 실습 코드

@PostMapping("/multiply") // POST /multiply
public int multiplyTwoNumbers(@RequestBody CalculatorMultiplyRequest request) {
    return request.getNumber1() * request.getNumber2();
}

@RequestBody 가 적용된 위의 코드를 포스트맨에서 실행하는 과정은 다음과 같다.

  • 포스트맨에서 스프링부트로 요청한다.

  • 스프링부트에서 곱셈을 수행하고 결과를 포스트맨으로 응답한다.

@RequestBody는 HTTP Body 에 있는 데이터를 자바로 매핑을 도와주는 어노테이션이다.

  • 7강. 유저 생성 API 개발

    유저 도메인을 생성하고 컨트롤러를 구현한다.

    • 유저 컨트롤러 구현하기

    • 유저 도메인 구현하기

    • 유저 dto 구현하기

  • 8강. 유저 조회 API 개발과 테스트

    유저 조회 API 스펙은 다음과 같다.

    • HTTP Method : GET HTTP Path : /user 쿼리 : 없음 반환 결과

      [{
      "id" : Long,
      "name" : String (null 불가능),
      "age" : Integer
      }, ... ]

      결과 반환이 리스트 안의 객체들이 저장된 JSON 형식이다. 그 이유는 Controller에서 getter 메서드가 있는 객체를 반환하면JSON이 된다. 새롭게 id 가 추가되는데, id는 유저별로 겹치지 않는 식별 번호다. List에 있는 유저의 순서를 id로 해주자!

  • 9강. Section1 정리. 다음으로!

    지금까지 구현한 문제점은 종료했다가 다시 재시작하면 모든 정보가 메모리에서만 유지되기 때문에 재시작하는 경우 모든 정보가 서버 종료와 함께 초기화되고 사라지게 된다. 이 문제를 해결하기 위해 데이터베이스 채택을 고려하자.

3.3 기본적인 데이터베이스 사용법강의 (10~13강)

  • 10강. Database와 MySQL

    인텔리제이에서 mySQL 접속하기

🌈 11강. MySQL에서 테이블 만들기

MySQL 에 [ root / mysql ] 으로 로그인하여 library데이터베이스(스키마) 에서 테이블을 생성하여 학습을 진행한다.

11.1 MySQL 타입 살펴보기 - 정수타입

  • tinyint : 1바이트

  • int : 4바이트

  • bigint 8바이트

11.2 MySQL 타입 살펴보기 - 실수타입

  • double : 8바이트

  • decimal(A, B) : 소수점을 B개 가지고 있는 전체 A 자릿수 실수

    ex) decimal(4, 2) : 12.23

11.3 MySQL 타입 살펴보기 - 문자열타입

  • char(A) : A 글자가 들어갈 수 있는 문자열 (고정된 크기)

  • varchar(A) : 최대 A 글자가 들어갈 수 있는 문자열 (가변 크기)

11.4 MySQL 타입 살펴보기 - 날짜, 시간타입

  • date : 날짜, yyyy-MM-dd

  • time : 시간, HH:mm:ss

  • datetime : 날짜와 시간을 합친 타입, yyyy-MM-dd HH:mm:ss

11.5 실습 코드

create database library default character set utf8;
​
use library;
​
show databases;
​
create table fruit
(
    id           bigint auto_increment,
    name         varchar(20),
    price        int,
    stocked_date date,
    primary key (id)
);

오늘 학습한 내용은 모두 DDL(Data Definition Langauge) 라고 한다. DDL은 데이터베이스 스키마를 정의하는 일련의 SQL 명령으로 생성, 수정, 삭제 등을 수행하고 CREATE, ALTER, DROP, TRUNCATE 가 있다.

🌈 12강. 테이블의 데이터를 조작하기

fruit 테이블에 CRUD 수행을 한다.

  • C(Create)R(Read)U(Update)D(Delete) 의 약자를 의미한다.

12.1 데이터 추가하는 쿼리문

insert into [테이블이름](필드1이름, 필드2이름) values(값1, 값2, ...)

12.2 fruit 테이블에 데이터 추가

id 의 경우는 auto_increment 로 설정했기 때문에 데이터를 추가하지 않아도 자동적으로 추가됩니다.

insert into fruit (name, price, stocked_date)
values ('사과', 1000, '2023-01-01');

12.3 fruit 모두 조회하기

select * from fruit

12.4 fruit의 이름(name)과 가격(price) 조회하기

select name, price from fruit

12.5 데이터 조회에 조건을 설정하기

select * from [테이블 이름] where [조건];

12.6 fruit에서 이름이 사과이거나 가격이 1000인 모든 값을 조회하기

select * from fruit where name= '사과' or price = 1000;

12.7 fruit에서 이름이 사과 또는 수박인 과일의 모든 값을 조회하기

select * from fruit where name in ('사과', '수박');

12.8 fruit에서 이름이 사과가 아닌 과일 조회하기

select * from fruit where name not in ('사과');

12.9 데이터 업데이트하기

update [테이블 이름]
set [필드1이름=값; 필드2이름=값, ...]
where [조건];

12.10 fruit 에서 이름이 사과이면 가격을 1500으로 수정하기

update fruit
set price = 1500
where name = '사과';

주의 사항은 만약 [조건]을 붙이지 않으면 모든 데이터가 수정된다는 것을 기억하세요.

12.11 데이터 삭제하기

delete from [테이블 이름]
where [조건]

12.12 fruit 테이블에서 이름이 사과이면 삭제하기

select from fruit where name = '사과';

🌈13강. Spring에서 Database 사용하기

오늘의 핵심은 JdbcTemplate 을 사용하여 데이터베이스와 연동을 수행했다.

  • JdbcTemplate 으로 유저 정보 저장하기

  • 데이터베이스에 저장된 정보 조회하기

3.4 데이터베이스를 사용해 만드는 API강의 (14~16강)

14강. 유저 업데이트 API, 삭제 API 개발과 테스트

JdbcTemplate 를 활용하여 유저를 등록하고 삭제할 수 있도록 유저 컨트롤러를 수정한다.

  • 유저 등록 구현하기

  • 유저 삭제 구현하기

15강. 유저 업데이트 API, 삭제 API 예외 처리 하기

API 에서 예외를 던지면 어떻게 되는지 간단한 API 를 생성해서 테스트를 수행한다.

  • 포스트맨에서 500 Internal Server Error 발생 확인하기

  • API에서 데이터 존재 여부를 확인하여 예외를 던지기

16강. Section2 정리. 다음으로!

한 개의 컨트롤러에서 많은 수행이 발생되는 문제를 해결하기 위해 고민해야 한다. 다음 장에서는 고민을 해결하는 시간을 갖는다.

3.5 클린코드의 개념과 첫 리팩토링강의 (17~18강)

🌈17강. 좋은 코드(Clean Code)는 왜 중요한가?!

이번 섹션 [17강 ~22강] 에서 학습 내용의 목표는 다음과 같다.

  1. 좋은 코드가 왜 중요한지 이해하고,원래 있던 Controller 코드를 보다 좋은 코드로 리팩토링한다.

  2. 스프링 컨테이너와 스프링 빈이 무엇인지 이해한다.

  3. 스프링 컨테이너가 왜 필요한지,좋은 코드와 어떻게 연관이 있는지 이해한다.

  4. 스프링 빈을 다루는 여러 방법을 이해한다.

코드를 읽는 것은 필수적이고 피할 수 없다. 대부분 회사에서 코드를 작성하는 시간보다 코드를 보고 이해하는 시간이 상당히 많은 것은 사실이다. 따라서 동료 혹은 내가 개발한 코드에 대해 이해를 돕기 위해 클린 코드에 대해 학습할 필요가 있다.

17.1 Controller에서 모든 기능을 구현하면 안되는 이유

  • 함수는 최대한 작게 만들고 한 가지 일만 수행하는 것이 좋다.

  • 클래스는 작아야 하며 하나의 책임 만을 가져야 한다.

17.2 우리가 작성한 Controller 함수 1개가 3000줄을 넘으면?!

  1. 그 함수를 동시에 여러 명이 수정할 수 없다.

  2. 그 함수를 읽고, 이해하는 것이 너무 어렵다.

  3. 그 함수의 어느 부분을 수정하더라도 함수 전체에 영향을 미칠수 있기 때문에 함부로 건들 수 없게 된다.

  4. 너무 큰 기능이기 때문에 테스트도 힘들다.

  5. 종합적으로 유지 보수성이 매우 떨어진다.

17.3 지금까지 구현한 controller 코드의 문제점

image

17.4 [1] API의 진입 지점으로써 HTTP Body를 객체로 변환한다.

image

17.5 [2] 현재 유저가 있는지 없는지 확인하고 예외 처리를 한다.

image

17.6 [3] SQL을 사용해 실제 Database와의 통신을 담당한다.

다음 강의에서 Controller 에 구현한 세 가지 기능을 분리할 것이다.

  • [1] API의 진입 지점으로써 HTTP Body를 객체로 변환한다.

  • [2] 현재 유저가 있는지 없는지 확인하고 예외 처리를 한다.

  • [3] SQL을 사용해 실제 Database와의 통신을 담당한다.

🌈 18강. Controller를 3단 분리하기 - Service와 Repository

Controller의 함수 1개가 하고 있던 역할

  1. API의 진입지점으로써 HTTP Body를 객체로 변환하고 있다. - controller 역할

  2. 현재 유저가 있는지, 없는지 등을 확인하고 예외 처리를 해준다. - service 역할

  3. SQL을 사용해 실제 DB 와의 통신을 담당한다. - repository 역할

한가지 궁금한 점을 남기면서 다음 강의 주제에 대해 미리 고민해보자.

  • Controller에서 JdbcTemplate은 어떻게 가져온 것일까?

4. 과제 내용 요약

깃허브 Wiki 에서 모든 과제 정리를 확인할 수 있습니다. 📚깃허브 Wiki로 이동하기

4.1 첫 번째 과제! (진도표 1일차)

어떤 관점에서 접근했는지 : 책과 강의 중심으로 기본 내용 위주로 정리

문제를 해결하는 과정은 무엇이었는지 : 아래에 정리

왜 그런 식으로 해결했는지 : 기본을 모르고 사용하면 무의미라고 생각

Q. 어노테이션을 사용하는 이유 (효과) 는 무엇일까?

어노테이션(Annotation)은 자바 프로그래밍 언어에서 메타데이터를 표현하는 방법 중 하나입니다. 즉, 프로그램에 대한 데이터를 프로그램 자체에 포함시키는 것입니다. 어노테이션은 컴파일러에게 정보를 제공하거나 코드를 분석하고 처리하는 데 사용됩니다.

어노테이션은 @ 기호를 사용하여 선언되며, 클래스, 메서드, 필드 및 다른 프로그램 요소에 적용될 수 있습니다. 어노테이션은 일종의 주석으로도 볼 수 있지만, 주석과는 달리 프로그램에 대한 추가 정보를 제공하고 프로그램의 행동을 변경할 수 있습니다.

어노테이션은 다양한 용도로 사용될 수 있습니다. 주요 용도는 다음과 같습니다.

  1. 컴파일러 지시자: 어노테이션을 사용하여 컴파일러에게 특정 경고를 무시하도록 지시하거나, 코드를 자동 생성하도록 지시할 수 있습니다.

  2. 런타임 처리: 어노테이션을 사용하여 런타임에 특정 기능을 활성화하거나 비활성화하거나, 특정 조건을 검사할 수 있습니다.

  3. 문서화: 어노테이션을 사용하여 코드를 문서화하거나, 코드의 목적이나 사용법을 설명할 수 있습니다.

  4. 코드 분석 및 검증: 어노테이션을 사용하여 코드를 분석하고 검증하는데 활용할 수 있습니다.

예를 들어, 스프링 프레임워크에서는 @Controller, @Autowired, @RequestMapping 등의 어노테이션을 사용하여 컴포넌트를 식별하고 의존성을 주입하며, 요청 매핑을 정의합니다. 이러한 어노테이션을 사용하여 스프링이 애플리케이션을 자동으로 구성하고 관리할 수 있습니다.

Q. 나만의 어노테이션은 어떻게 만들 수 있을까?

새로운 어노테이션을 정의하는 방법은 @ 기호를 붙이는 것을 제외하면 인터페이스를 정의하는 것과 동일하다.

@interface 어노테이션이름{
    타입 요소이름(); // 어노테이션의 요소를 선언한다.
    . . .
}

어노테이션의 요소

  • 어노테이션의 요소(element)는 어노테이션 내에 선언된 메서드를 말한다.

  • 어노테이션에도 인터페이스처럼 상수를 정의할 수 있지만, 디폴트 메서드는 정의할 수 없다.

  • 어노테이션의 요소는 반환 값이 있고, 매개 변수는 없는 추상 메서드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다.

  • 단, 어노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해야 한다. 요소의 이름도 같이 작성하기 때문에 순서는 상관없다.

다음 예제는 어노테이션의 선언과 사용 예제입니다:

@interface TestInfo {
    int count();
    String testedBy();
    String[] testTools();
    TestType testType(); // enum TestType { FIRST, FINAL }
    DateTime testDate(); // 자신이 아닌 다른 어노테이션(@DateTime)을 포함할 수 있다.
}
​
@interface DateTime {
    String yymmdd();
    String hhmmss();
}

// 애너테이션 사용. 요소의 값을 타입에 맞게 전부 적어주어야 한다.
@TestInfo(
    count=3, testedBy="Kim",
    testTools={"JUnit", "AutoTester"},
    testType=TestType.FIRST,
    testDate=@DateTime(yymmdd="160101", hhmmss="235959")
)
public class NewClass { ... }

어노테이션 요소의 기본 값

  • 어노테이션의 각 요소는 기본값을 가질 수 있다.

  • 기본값이 있는 요소는 어노테이션을 적용할 때 지정하지 않으면 기본값이 사용된다.

  • 기본값으로 null을 제외한 모든 리터럴이 가능하다.

  • 어노테이션의 요소가 하나이고 이름이 value인 경우, 어노테이션을 적용할 때 요소의 이름을 생략 하고 값만 적어도 된다.

  • 요소의 타입이 배열인 경우, 중괄호 {}를 사용해서 여러 개의 값을 지정할 수 있다.

  • 기본값을 지정할 때도 중괄호{}를 사용할 수 있다.

@interface TestInfo {
    int count() default 1;  // 기본값을 1로 지정
}
​
@TestInfo  // @TestInfo(count = 1)과 동일
public class NewClass { ... }

// 어노테이션의 요소가 오직 1개에 이름이 value인 경우, 어노테이션 요소의 이름을 생략하고 값만 작성해도 된다.
@interface TestInfo {
    String value();
}
​
@TestInfo("passed")  // @TestInfo(value="passed")와 동일
class NewClass { ... }

// 요소의 타입이 배열인 경우, 괄호 {}를 사용해서 여러 개의 값을 지정할 수 있다.
@interface TestInfo{
    String[] testTools();
}   
​
@TestInfo(testTools={"Junit", "AutoTester"}) // 값이 여러 개인 경우
@TestInfo(testTools="Junit") // 값이 하나일 때는 괄호 {} 생략 가능
@TestInfo(testTools={})  // 값이 없을 때는 괄호{}가 반드시 필요

// 기본값을 지정할 때도 마찬가지로 괄호 {}를 사용할 수 있다.
@interface TestInfo {
    String[] info() default {"aaa","bbb"};  
    String[] info2() default "ccc"; // 기본값이 하나인 경우 괄호 생략가능.
}
​
@TestInfo   // @TestInfo(info={"aaa","bbb"}, info2="ccc") 와 동일
@TestInfo(info2={}) // @TestInfo(info={"aaa","bbb"}, info2={}) 와 동일
class NewClass { ... }

// 요소의 타입이 배열일 때도 요소의 이름이 value이면, 요소의 이름을 생략할 수 있다.
@interface SuppressWarnings {
    String[] value();
}
​
// @SuppressWarnings(value = {"deprecation", "unchecked"})
@SuppressWarnings({"deprecation", "unchecked"})
class NewClass { ... }


java.lang.annotation.Annotation

모든 어노테이션의 조상은 Annotation 이다. 그러나 어노테이션은 상속이 허용되지 않으므로 아래와 같이 명시적으로 Annotation을 조상으로 지정할 수 없다.

@interface TestInfo extends Annotation { // 에러. 허용되지 않는 표현
    int count();
    String testedBy();
    ...
}

그리고 아래의 코드에서 볼 수 있듯이 Annotation은 어노테이션이 아니라 일반적인 인터페이스로 정의되어 있다.

package java.lang.annotation;
​
public interface Annotation { // Annotation 자신은 인터페이스이다.
    boolean equals(Object obj);
    int hashCode();
    String toString();
​
    Class<? extends Annotation> annotationType(); // 어노테이션 타입 반환

Annotation 인터페이스 안의 메소드들은 추상 메소드이지만, 컴파일러가 자동으로 내용을 구현해주기 때문에 메소드 사용 가능. 모든 어노테이션의 조상이므로 모든 어노테이션 객체에 대해 equals(), hashCode(), toString() 과 같은 메서드를 호출하는 것이 가능하다.


마커 어노테이션 - Marker Annotation 값을 지정할 필요가 없는 경우, 어노테이션의 요소를 하나도 정의하지 않을 수 있다. Serializable 이나 Cloneable 인터페이스처럼, 요소가 하나도 정의되지 않은 어노테이션을 마커 어노테이션이라고 한다.

@Target(ElementType.METHOD)
@Retension(RetentionPolicy.SOURCE)
public @interface Override {} // 마커 어노테이션. 요소가 0개
​
@Target(ElementType.METHOD)
@Retension(RetentionPolicy.SOURCE)
public @interface Test {} // 마커 어노테이션. 요소가 0개

어노테이션 요소의 규칙 어노테이션의 요소를 선언할 때 아래의 규칙을 반드시 지켜야 한다.

  • 요소의 타입은 기본형, String, enum, 어노테이션, Class 만 허용된다.

  • 괄호 안에 매개변수를 선언할 수 없다.

  • 예외를 선언할 수 없다.

  • 요소를 타입 매개변수로 정의할 수 없다.

아래의 코드에서 오른쪽 주석을 통해 잘못된 부분을 알아본다.

@interface AnnoTest {
    int id = 100; // OK. 상수 들어갈 수 있다. static final int id = 100;
    String major(int i, int j); // 에러. 매개변수 들어갈 수 없다.
    String minor() throws Exception; // 에러. 예외 처리 불가능
    ArrayList<T> list(); // 에러. 요소의 타입에 타입 매개변수 정의 불가능
}


위의 방법을 통해서 간단하게 어노테이션을 구현하면 다음과 같습니다:

import java.lang.annotation.*;
​
@Retention(RetentionPolicy.RUNTIME) // 어노테이션 유지 정책 설정
@Target(ElementType.METHOD) // 어노테이션이 메서드에 적용되도록 지정
public @interface MyAnnotation {
    String value(); // 속성 정의
}

  1. @interface 키워드 사용: 어노테이션을 정의하기 위해 @interface 키워드를 사용합니다. 이 키워드를 사용하여 새로운 어노테이션을 선언하고 그 내부에 속성을 정의할 수 있습니다.

  2. 속성 정의: 어노테이션 내에서 사용할 속성을 정의합니다. 속성은 메서드처럼 선언되며, 반환 유형과 속성 이름이 있습니다. 이러한 속성을 사용하여 어노테이션에 추가 정보를 제공할 수 있습니다.

  3. Retention 정책 설정 (optional): 어노테이션을 정의할 때 Retention 정책을 설정할 수 있습니다. Retention 정책은 어노테이션이 유지될 시점을 결정합니다. RetentionPolicy 열거형에는 SOURCE, CLASS, RUNTIME 세 가지 정책이 있으며, 각각 컴파일 시, 클래스 로딩 시, 런타임 시에 어노테이션이 유지됩니다. 기본적으로는 RUNTIME 정책이 적용됩니다.

위의 코드에서 @interface 키워드를 사용하여 MyAnnotation이라는 새로운 어노테이션을 정의하였습니다. 이 어노테이션은 메서드에 적용될 수 있도록 @Target(ElementType.METHOD)를 설정하였고, 런타임 시에 어노테이션이 유지되도록 @Retention(RetentionPolicy.RUNTIME)을 설정하였습니다. value()라는 속성을 정의하였습니다.

이제 위에서 정의한 어노테이션을 사용할 수 있습니다:

public class MyClass {
​
    @MyAnnotation(value = "Hello")
    public void myMethod() {
        // 메서드 내용
    }
}

위의 코드에서 myMethod() 메서드에 @MyAnnotation 어노테이션을 적용하고, 속성 값으로 "Hello"를 전달하였습니다. 이렇게 정의된 어노테이션은 런타임 시에 메서드에서 해당 어노테이션 정보를 읽어올 수 있습니다.

4.2 두 번째 과제! (진도표 2일차)

어떤 관점에서 접근했는지 : 강의 중심으로 정리

문제를 해결하는 과정은 무엇이었는지 : 아래에 정리

왜 그런 식으로 해결했는지 : 강의에서 벗어난 방식으로 구현하고 싶지 않았음

문제 1

image

Ex01Controller

package com.group.libarayapp.controller.task;
​
import com.group.libarayapp.dto.task.request.Ex01Request;
import com.group.libarayapp.dto.task.response.Ex01Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class Ex01Controller {
​
    @GetMapping("/api/v1/calc")
    public Ex01Response firstEx(Ex01Request request) {
        return new Ex01Response(request);
    }
}

Ex01Request

package com.group.libarayapp.dto.task.request;
​
public class Ex01Request {
    private final int num1;
    private final int num2;
    public Ex01Request(int num1, int num2) {
        this.num1 = num1;
        this.num2 = num2;
    }
​
    public int getNum1() {
        return num1;
    }
​
    public int getNum2() {
        return num2;
    }
}

Ex01Response

package com.group.libarayapp.dto.task.response;
​
import com.group.libarayapp.dto.task.request.Ex01Request;
​
public class Ex01Response {
​
    private final int add;
    private final int minus;
    private final int multiply;
​
​
    public Ex01Response(Ex01Request ex01Request) {
        this.add = ex01Request.getNum1() + ex01Request.getNum2();
        this.minus = ex01Request.getNum1() - ex01Request.getNum2();
        this.multiply = ex01Request.getNum1() * ex01Request.getNum2();
    }
​
    public int getAdd() {
        return add;
    }
​
    public int getMinus() {
        return minus;
    }
​
    public int getMultiply() {
        return multiply;
    }
}

Ex01Controller에서 쿼리 파라미터를 입력 받을 수 있도록 public Ex01Response firstEx(Ex01Request request) 으로 메서드명 선언한다.

구체적으로, firstEx 메서드의 매개변수인 Ex01Request 클래스에 두 개의 입력 받은 쿼리 파라미터인 num1, num2 가 저장되고, Ex01Response 클래스의 생성자의 매개변수로 Ex01Request 를 작성하여 요구사항에 따라 두 수의 합, 뺄셈, 곱을 구한다.

계속 해메고 있는 문제가 JSON 형식으로 다시 출력할 때, 키의 이름은 getter 메서드명에 의해 정해지는 것을 계속 해메고 나서 알게 됐다.


문제 2

image

Ex02Controller

package com.group.libarayapp.controller.task;
​
import com.group.libarayapp.dto.task.request.Ex02Request;
import com.group.libarayapp.dto.task.response.Ex02Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class Ex02Controller {
​
    @GetMapping("/api/v1/day-of-the-week")
    public Ex02Response secondEx(Ex02Request request) {
        return new Ex02Response(request);
    }
}

Ex02Request

package com.group.libarayapp.dto.task.request;
​
import java.time.LocalDate;
​
public class Ex02Request {
​
    private final LocalDate date;
​
    public Ex02Request(LocalDate date) {
        this.date = date;
    }
​
    public LocalDate getDate() {
        return date;
    }
}

Ex02Response

package com.group.libarayapp.dto.task.response;
​
import com.group.libarayapp.dto.task.request.Ex02Request;
​
import java.time.format.TextStyle;
import java.util.Locale;
​
public class Ex02Response {
​
    private final String dayOfTheWeek;
​
    public Ex02Response(Ex02Request request) {
        this.dayOfTheWeek = request.getDate().getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase();
    }
​
    public String getDayOfTheWeek() {
        return dayOfTheWeek;
    }
}

한 개의 쿼리 파라미터를 받는 문제이기 때문에 @RequestParam 사용을 고려했지만, 별도의 dto 를 생성하는 경우가 더욱 많을 것으로 생각되기 때문에 연습할 겸 Ex02Request 클래스에 쿼리 파라미터를 받을 수 있도록 생성했다.

LocalDate 는 날짜 정보만 제공하는데 사용되는 클래스이다. 참고한 자료는 https://www.tcpschool.com/java/java_time_localDateTime 이다.

문제 1번과 동일한 방식과 동일하다. Ex02Response 의 생성자의 매개변수를 쿼리 파라미터가 저장된 Ex02Request 으로 작성하고, 쿼리 파라미터인 날짜 정보를 알맞은 포메팅 형식으로 변환한다. 그리고 getter 메서드 이름에 유의하여 키 이름과 동일한 dayOfTheWeek 으로 하자.


문제 3

image

Ex03Controller

package com.group.libarayapp.controller.task;
​
​
import com.group.libarayapp.dto.task.request.Ex03Request;
import com.group.libarayapp.dto.task.response.Ex03Response;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class Ex03Controller {
​
    @PostMapping("/api/v1/numbers-sum")
    public int thirdEx(@RequestBody Ex03Request request) {
        Ex03Response result = new Ex03Response(request.getNumbers());
        return result.getSum();
    }
}

Ex03Request

package com.group.libarayapp.dto.task.request;
​
import java.util.ArrayList;
import java.util.List;
​
public class Ex03Request {
​
    private final List<Integer> numbers = new ArrayList<>();
​
    public List<Integer> getNumbers() {
        return numbers;
    }
​
​
}

Ex03Response

package com.group.libarayapp.dto.task.response;
​
import java.util.List;
​
public class Ex03Response {
    private int sum;
​
    public Ex03Response(List<Integer> numbers) {
        for (int number : numbers) {
            sum += number;
        }
    }
​
    public int getSum() {
        return sum;
    }
}

문제 1, 문제 2 와 방식이 다르다. 지금까지 인풋으로 쿼리 파라미터를 보냈지만, 문제 3은 JSON을 인풋으로 넣고 반환 타입은 정수이다.

참고로 JSON기반의 메시지를 사용하는 요청의 경우에는 @RequestBody 십중팔구 사용한다는 것을 기억하자. @RequestBody를 사용하면, HTTP Body 안의 데이터를 자바 객체로 변환하여 매핑된 메서드 파라미터로 전달한다.

그래서 Ex03Request 안에 여러 숫자를 저장하기 위한 List가 있다. 그리고 지난 문제들과 동일한 방식으로 모든 처리는 response 에서 수행한다. Ex03Response 에서 입력된 모든 수를 더한다. 이때, 어떠한 숫자도 입력이 안된 상황이거나 null인 경우에 대한 예외는 작성하지 않고 넘어갔다. 배우지 않은 다른 것들을 생각하기 보다는 학습한 내용을 중점적으로 복습할 것이다.

4.3 세 번째 과제! (진도표 3일차)

어떤 관점에서 접근했는지 : 책과 강의 중심으로 정리

문제를 해결하는 과정은 무엇이었는지 : 아래에 정리

왜 그런 식으로 해결했는지 : 기본을 모르고 사용하면 무의미라고 생각

1. 자바의 람다식은 왜 등장했을까?

자바의 람다식(lambda expression)은 자바 8에서 도입되었으며, 그 등장 배경에는 여러 이유가 있습니다. 람다식은 간결한 코드 작성을 가능하게 하여 자바의 표현력을 크게 향상시켰고, 함수형 프로그래밍 패러다임을 자바에 도입하여 더 유연하고 효율적인 프로그래밍 방식을 제공합니다. 람다식이 등장한 주요 이유는 다음과 같습니다:

  1. 코드의 간결성: 람다식을 사용하면 익명 클래스를 사용할 때보다 훨씬 더 간결한 코드로 이벤트 리스너나 콜백과 같은 함수형 인터페이스의 인스턴스를 생성할 수 있습니다. 이는 코드의 가독성을 크게 향상시키고, 개발자가 더 중요한 로직에 집중할 수 있게 해줍니다.

  2. 함수형 프로그래밍의 도입: 자바 8 이전 버전은 주로 객체 지향 프로그래밍 패러다임을 따랐습니다. 람다식의 도입으로 자바는 함수형 프로그래밍 개념을 효과적으로 통합하여, 개발자가 상태 변경이나 가변 데이터를 피하는 순수 함수형 프로그래밍 스타일을 채택할 수 있게 되었습니다. 이는 병렬 처리와 이벤트 처리 코드를 더 쉽고 효율적으로 작성할 수 있게 해줍니다.

  3. 병렬 처리의 용이성: 자바 8에서는 스트림 API도 함께 도입되었습니다. 람다식과 스트림 API의 조합은 컬렉션 처리를 위한 선언적인 접근 방식을 제공하여, 병렬 처리를 쉽게 구현할 수 있게 해줍니다. 이는 멀티코어 프로세서의 이점을 활용하여 성능을 향상시킬 수 있는 중요한 기능입니다.

  4. API의 일관성과 유연성 향상: 람다식을 통해 자바의 기존 API들은 더 유연하고 강력한 방식으로 확장될 수 있게 되었습니다. 예를 들어, java.util.Collection 인터페이스에 새롭게 추가된 forEach, removeIf 같은 메소드들은 람다식을 이용하여 보다 쉽게 사용할 수 있게 되었습니다.

람다식의 도입은 자바를 더 현대적이고, 표현력이 풍부하며, 다양한 프로그래밍 스타일을 지원하는 언어로 변모시켰습니다. 이러한 변화는 자바가 계속해서 발전하고 현대적인 프로그래밍 요구사항에 부응할 수 있게 하는 데 중요한 역할을 했습니다.


2. 람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까?

2.1 람다식과 익명 클래스의 관계

  1. 구현 방식의 차이

    • 람다식은 익명 클래스를 사용하는 것보다 훨씬 간결합니다.

    • 람다식은 컴파일러에 의해 익명 클래스로 변환될 수 있지만, 람다식이 제공하는 간결성과 명확성은 익명 클래스보다 뛰어납니다.

  2. 사용 범위의 차이

    • 람다식은 오직 함수형 인터페이스에만 사용될 수 있습니다.

    • 익명 클래스는 함수형 인터페이스 뿐만 아니라, 추상 클래스나 구체 클래스의 하위 클래스를 만드는 데에도 사용될 수 있습니다.

  3. this 키워드의 차이

    • 람다식 내부에서 this 키워드는 람다식을 감싸는 클래스를 가리킵니다.

    • 익명 클래스 내부에서 this익명 클래스 자신을 가리킵니다.

  4. 직렬화

    • 익명 클래스 인스턴스는 직렬화할 수 있지만(해당 클래스가 직렬화를 지원한다면)

    • 람다식은 직렬화에 대해 명시적으로 설계되지 않았습니다. 람다식의 직렬화는 권장되지 않으며, 사용 시 주의가 필요합니다.

람다식과 익명 클래스 사이의 이러한 차이점들은 자바에서 특정 상황에 맞는 가장 적절한 도구를 선택하는 데 도움을 줍니다. 람다식은 간결성과 명확성 때문에 많은 경우 익명 클래스를 대체하게 되었지만, 익명 클래스가 여전히 유용한 상황이 있습니다.


2.2 this 키워드, 사용 범위의 차이

  1. this 키워드의 차이

    • this 키워드는 익명 클래스와 람다식에서 다르게 작동합니다.

익명 클래스 예제

public class AnonymousClassExample {
    private String message = "익명 클래스 메시지";
​
    public void doWork() {
        Runnable r = new Runnable() { // 익명 클래스 생성 
            private String message = "Runnable 메시지";
​
            @Override
            public void run() {
                System.out.println(this.message); // this.message는 익명 클래스 내부의 message를 의미
            }
        };
        r.run();
    }
​
    public static void main(String[] args) {
        new AnonymousClassExample().doWork();
    }
}

위 익명 클래스 예제에서 this.message는 익명 클래스 내부의 message 변수를 가리킵니다. 따라서 출력은 "Runnable 메시지"가 됩니다.

람다식 예제

public class LambdaExample {
    private String message = "람다 메시지";
​
    public void doWork() {
        Runnable r = () -> System.out.println(this.message); // 람다식 사용
        r.run();
    }
​
    public static void main(String[] args) {
        new LambdaExample().doWork();
    }
}

위 람다식 예제에서 this.message는 람다식을 감싸는 LambdaExample 클래스의 인스턴스 변수 message를 가리킵니다. 따라서 출력은 "람다 메시지"가 됩니다.


  1. 사용 범위의 차이

    • 함수형 인터페이스와 익명 클래스 모두를 사용할 수 있는 상황익명 클래스만 사용할 수 있는 상황의 예를 들어보겠습니다.

함수형 인터페이스 사용 예제

함수형 인터페이스인 Runnable에 대한 람다식익명 클래스의 사용 예:

  • 람다식

    Runnable rLambda = () -> System.out.println("람다식 실행");
    rLambda.run();

     

  • 익명 클래스

    Runnable rAnonymous = new Runnable() {
        @Override
        public void run() {
            System.out.println("익명 클래스 실행");
        }
    };
    rAnonymous.run();

     

위 예제에서는 람다식과 익명 클래스 모두 Runnable을 구현할 수 있습니다.

익명 클래스만 사용할 수 있는 상황 예제

익명 클래스를 사용하여 추상 클래스의 추상 메소드를 구현하는 경우:

abstract class MyAbstractClass {
    abstract void myMethod();
}
​
public class Test {
    public static void main(String[] args) {
        MyAbstractClass myObj = new MyAbstractClass() {
            @Override
            void myMethod() {
                System.out.println("추상 클래스의 메소드 구현");
            }
        };
        myObj.myMethod();
    }
}

이 경우, MyAbstractClass함수형 인터페이스가 아니므로 람다식을 사용할 수 없습니다. 따라서 이런 상황에서는 익명 클래스를 사용해야 합니다.

이 예제들을 통해 this 키워드의 차이와 사용 범위의 차이를 이해할 수 있습니다. 익명 클래스는 더 일반적인 사용 사례에 적합할 수 있지만, 람다식은 코드를 더 간결하고 명확하게 만드는 데 유리합니다.

2.3 인터페이스의 익명 클래스와 람다 표현식

// 익명 클래스로 작성하기
Operate operate = new Operate() {
    public int perate(int a, int b) {
        return a + b;
    }
};

// 위의 코드를 람다식으로 표현하기
Operate operate = (a, b) -> {
    return a + b;
};

람다식으로 표현하면 코드를 작성해야 하는 글자 수가 줄어들고, 간결하게 표현이 가능합니다. 해당 코드를 함수형 프로그래밍에서는 람다 또는 익명 함수라고도 부릅니다.

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator(20, 10);
        int result = calculator.result((a, b) -> {
            return a + b;
        });
        System.out.println(result); // 30
        
        int result2 = calculator.result((a, b) -> {
            return a - b;
        });
        System.out.println(result2); // 10
    }
}

이전의 코드보다 람다를 이용한 코드 작성이 코드 작성 수도 적어지고 보기도 편해질 수 있습니다.

Operate operate = (a, b) -> {
    return a + b;
};

이전 코드보다 람다는 더 간단히 작성이 가능합니다.

Operate operate = (a, b) -> a + b;

이렇게 줄일 수 있는 경우는 반환값이 있는 람다이어야 하고, 람다 내부 구문이 코드로 한 줄 작성이 가능해야 하며 return을 명시하지 않아도 인지할 수 있도록 연산 과정이 한 줄로 작성되어야 합니다.

// 이러한 코드는 더 간단히 작성될 수 없다.
Operate operate = (a, b) -> {
    System.out.println("Operate");
    return a + b;
} 

이러한 람다 구문은 더 간단히 작성될 수 없습니다. 쉽게 얘기하자면 세미콜론; 이 두 번 찍히는 내부 구문은 더 줄일 수 없다고 보시면 됩니다.

람다 Lambda

해당 그림은 익명 클래스 인스턴스와 람다와의 연관 관계를 작성한 그림입니다. 변환 과정을 좀 더 한 눈에 볼 수 있습니다.

2.4 람다 표현식의 제한

  • 참고로 람다 표현식을 사용하기 위해서는 다음의 두 가지 제약 조건을 모두 만족해야 합니다.

    1. 인터페이스이어야 한다.

    2. 인터페이스에는 하나의 추상 메서드만 선언되어야 한다.

public interface Operate {
    // 오버라이드 해야 할 메서드가 두 개인 경우에는 람다 표현식이 불가능하다.
    int operate(int a, int b);
    void print();
}

Operate operate = new Operate() {
    public int operate(int a, int b) {
        return a + b;
    }
    public void print() {
        System.out.println("출력");
    }
};

오버라이드 해야 할 추상 메서드가 두 개 이상인 경우는 람다 표현식을 사용할 수 없습니다. 람다는 하나의 행위만을 사용한다고 가정하기 때문입니다.

public interface Operate {
    // 추상 메서드가 하나이다
    int operate(int a, int b);
​
    // default 메서드는 추상 메서드에 포함되지 않는다
    default void print() {
        System.out.println("출력");
    }
}

Operate operate = (a, b) -> {
    print();
    return a + b;
};

그러나 추상 메서드가 아닌 default 메서드가 포함된 경우는 람다 표현식이 가능합니다. 결론적으로 오버라이드 해야 하는 추상 메서드는 하나이기 때문입니다.

2.5 람다의 컨셉

람다 Lambda

우리는 항상 매개 변수로 값을 전달한다는 개념으로 배웠습니다. 물론, 상수 값이나 인스턴스의 참조 값을 전달하는 것은 맞습니다. 그러나 생각을 확장할 필요가 있습니다. 람다를 무엇을 해야 한다는 행위로 본다면, 값이 아니라 행위를 전달한다고 볼 수 있습니다.

익숙하지 않은 경우 개념 자체가 어려울 수 있습니다. 그래도 괜찮습니다. 최대한 익숙하게 사용하려고 하면 됩니다.

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator(20, 10);
​
        int result1 = calculator.result((a, b) -> {
            return a + b; // a + b를 하라는 행위 전달
        });
        
        int result2 = calculator.result((a, b) -> {
            return a - b; // a - b를 하라는 행위 전달
        });
​
        int result3 = calculator.result((a, b) -> {
            return a / b; // a / b를 하라는 행위 전달
        });
​
        int result4 = calculator.result((a, b) -> {
            return a * b; // a * b를 하라는 행위 전달
        });
    }
}

값을 전달한다는 개념보다는 '이렇게 해라' 라는 행위를 전달하는 개념은 함수형 프로그래밍으로 가기 위한 기초입니다. 그러나 우리는 아직 함수형 프로그래밍이라는 것에 대해 뭔지도 잘 알지 못할 뿐더러 익숙하지 않고 너무 많은 내용을 배우고 가기 때문에 머리속에 정리하기 힘든 과정이라는 점을 인지해야 합니다.


2.6 람다식의 문법은 어떻게 될까?

람다식 (Lambda expression)

자바가 1996년에 처음 등장한 이후로 두 번의 큰 변화가 있었는데, 한번은 JDK1.5부터 추가된 지네릭스(generics)의 등장이고, 또 한번은 JDK1.8부터 추가된 람다식(lambda expression)의 등장이다. 람다식의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다.

  1. 람다식이란?

람다식(Lambda expression)은 메서드를 하나의 식(expression)으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다. 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 익명 함수(anonymous function)이라고도 한다.

int[] arr = new int[5];
Arrays.setAll(arr, i -> (int)(Math.random() * 5) + 1);  //arr = [1, 5, 2, 1, 1]
​
//i -> (int)(Math.random() * 5) + 1
int method(int i) {
    return (int)(Math.random() * 5) + 1;
}

모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 이 메서드를 호출할 수 있다. 그러나 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 메서드의 역할을 대신할 수 있다.

람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다. 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.

메서드와 함수의 차이

객체지향개념에서는 함수(function) 대신 객체의 행위나 동작을 의미하는 메서드(method)라는 용어를 사용한다. 메서드는 함수와 같은 의미이지만, 특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미의 다른 용어를 선택해서 사용해 왔다. 그러나 이제 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 함수라는 용어를 사용하게 되었다.

  1. 람다식 작성하기

람다식은 익명 함수이므로 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 -> 를 추가한다.

반환타입 메서드이름(매개변수 선언) {
    문장들
}
​
/* 반환타입 메서드이름 */ (매개변수 선언) -> {
    문장들
}
​
/* Example : 두 값 중에서 큰 값을 반환하는 메서드 max */
int max(int a, int b) {
    retrun a > b ? a : b;
}
​
/* int max */ (int a, int b) -> {
    return a > b ? a : b;
}
​
/* 
    반환값이 있는 메서드의 경우, return문 대신 식(expression)으로 대신할 수 있다. 
    식의 연산결과가 자동적으로 반환값이 된다.
    이때는 문장(statement)이 아닌 식이므로 끝에 ;을 붙이지 않는다.
*/
​
(int a, int b) -> { return a > b ? a : b; }
(int a, int b) -> a > b ? a : b
​
/*
    람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있는데,
    대부분의 경우에 생략 가능하다.
    (int a, b) -> a > b ? a : b
    와 같이 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않는다.
*/
    
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b
​
/*
    선언된 매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있다.
    단, 매개변수의 타입이 있으면 괄호()를 생략할 수 없다.
*/
    
(a) -> a * a
(int a) -> a * a
a -> a * a  //OK
int a -> a * a  //에러
​
/*
    괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있다.
    이 때 문장의 끝에 ;을 붙이지 않는다.
*/
    
(String name, int i) -> {
    System.out.println(name + " = " + i);
}
(String name, int i) ->
    System.out.println(name + " = " + i)
    
/*
    괄호{} 안의 문장이 return문일 경우 괄호{}를 생략할 수 없다.
*/
    
(int a, int b) -> { return a > b ? a : b; } //OK
(int a, int b) -> return a > b ? a : b  //에러

3.메서드를 람다식으로 변환한 예제

//메서드1
int max(int a, int b) {
    return a > b ? a : b;
}
​
//람다식1
(int a, int b) -> { retrun a > b ? a : b; }
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b
​
​
//메서드2
void printVal(String name, int i) {
    System.out.println(name + " = " + i);
}
​
//람다식2
(String name, int i) -> { System.out.println(name + " = " + i); }
(name, i) -> { System.out.println(name + " = " + i); }
(name, i) -> System.out.println(name + " = " + i)
​
​
//메서드3
int square(int x) {
    return x * x;
}
​
//람다식3
(int x) -> x * x
(x) -> x * x
x -> x * x
​
//메서드4
int roll() {
    return (int)(Math.random() * 6);
}
​
//람다식4
() -> { return (int)(Math.random() * 6); }
() -> (int)(Math.random() * 6)
​
​
//메서드5
int sumArr(int[] arr) {
    int sum = 0;
    for (int i : arr)
        sum += i;
    return sum;
}
​
//람다식5
(int[] arr) -> {
    int sum = 0;
    for (int i : arr)
        sum += i;
    return sum;
}

4.함수형 인터페이스(Functional Interface)

람다식은 익명 클래스의 객체와 동등하다.

(int a, int b) -> a > b ? a : b
​
new Object() {
    int max(int a, int b) {
        return a > b ? a : b;
    }
}

함수형 인터페이스(functional interface) - 람다식을 다루기 위한 인터페이스

@FunctionalInterface
interface MyFunction {  //함수형 인터페이스 MyFunction을 정의
    public abstract int max(int a, int b);
}

함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다. 반면에 static메서드와 default메서드의 개수에는 제약이 없다.

  • @FunctionalInterface를 붙이면, 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해준다.

//인터페이스의 메서드 구현
List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");
​
Colldections.sort(list, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s2.compareTo(s1);
    }
});
​
//람다식
List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");
Collections.sort(list, (s1, s2) -> s2.compareTo(s1));

함수형 인터페이스 타입의 매개변수와 반환타입

//함수형 인터페이스 MyFunction 정의
@FunctionalInterface
interface MyFunction {
    void myMethod();    //추상 메서드
}

//메서드의 매개변수가 MyFunction타입이면, 
//이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다는 뜻이다.
void aMethod(MyFunction f) {    //매개변수의 타입이 함수형 인터페이스
    f.myMethod();   //MyFunction에 정의된 메서드 호출
}
​
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);
​
//또는 참조변수 직접 람다식을 매개변수로 지정하는 것도 가능하다.
aMethod(() -> System.out.println("myMethod()"));    //람다식을 매개변수로 지정
​
//메서드의 반환타입이 함수형 인터페이스타입이라면,
//이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나
//람다식을 직접 반환할 수 있다.
MyFunction myMethod() {
    MyFunction f = () -> {};
    return f;   //return () -> {};
}

람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미한다. 즉, 변수처럼 메서드를 주고받는 것이 가능해진 것이다.

5.람다식의 타입과 형변환

함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다. 정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없다. 그래서 대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요하다.

//interface MyFunction{void method();}
MyFunction f = (MyFunction)(() -> {});  //양변의 타입이 다르므로 형변환 필요 

람다식은 이름이 없을 뿐 객체인데도 분명하고 Object타입으로 형변환 할 수 없다. 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.

Object obj = (Object)(() -> {});    //에러. 함수형 인터페이스로만 형변환 가능
​
//Object타입으로 형변환하려면, 먼저 함수형 인터페이스로 변환해야 한다.
Object obj = (Object)(MyFunction)(() -> {});
String str = ((Object)(MyFunction)(() -> {})).toString();

일반적인 익명 객체라면, 컴파일러가 객체의 타입을 '외부클래스이름$번호'와 같은 형식으로 만들어내지만, 람다식의 타입은 '외부클래스이름$$Lambda$번호'와 같은 형식으로 만들어낸다.

외부 변수를 참조하는 람다식

람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 익명 클래스와 동일하다.

6 java.util.function패키지

java.util.function패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다. 매번 새로운 함수형 인터페이스를 정의하지 말고, 가능하면 이 패키지의 인터페이스를 활용하는 것이 좋다.

java.util.function패키지의 주요 함수형 인터페이스

img

매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스가 정의되어 있고, Function의 변형으로 Predicate가 있는데, 반환값이 boolean이라는 것만 제외하면 Function과 동일하다. Predicate는 조건식을 함수로 표현하는데 사용된다.

  • 타입 문자 T는 Type을, R은 Return Type을 의미한다.

조건식의 표현에 사용되는 Predicate

Predicate는 Function의 변형으로, 반환타입이 boolean이라는 것만 다르다. Predicate는 조건식을 람다식으로 표현하는데 사용된다.

Predicate<String> isEmptySTr = s -> s.length() == 0;
String s = "";
​
if (isEmptyStr.test(s)) //if(s.length() == 0)
    System.out.println("This is an empty String.");

매개변수가 두 개인 함수형 인터페이스

매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 Bi가 붙는다.

img

매개변수의 타입으로 보통 T를 사용하므로, 알파벳에서 T의 다음 문자인 U, V, W를 매개변수의 타입으로 사용하는 것일 뿐 별다른 의미는 없다. 두 개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어 써야 한다.

//3개의 매개변수를 갖는 함수형 인터페이스 선언
@FunctionalInterface
interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

UnaryOperator와 BinaryOperator

Function의 또 다른 변형으로, 매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.

  • UnaryOperator와 BinaryOperator의 조상은 각각 Function과 BiFunction이다.

img

컬렉션 프레임웍과 함수형 인터페이스

컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가되었는데, 그 중의 일부는 함수형 인터페이스를 사용한다.

인터페이스메서드설명Collectionboolean removeIf(Predicate fliter)조건에 맞는 요소를 삭제Listvoid replaceAll(UnaryOperator operator)모든 요소를 변환하여 대체Iterablevoid forEach(Consumer action)모든 요소에 작업 action을 수행MapV compute(K key, BiFunction<K, V, V> f)지정된 키의 값에 작업 f를 수행V computeIfAbsent(K key, Function<K, V> f)키가 없으면, 작업 f 수행 후 추가V computeIfPresent(K key, BiFunction<K, V, V> f)지정된 키가 있을 때, 작업 f 수행V merget(K key, V value, BiFunction<V, V, V> f)모든 요소에 병합작업 f를 수행void forEach(BiConsumer<K, V> action)모든 요소에 작업 action을 수행void replaceAll(BiFunction<K, V, V> f)모든 요소에 치환작업 f를 수행

기본형을 사용하는 함수형 인터페이스

img

7.Function의 합성과 Predicate의 결합

java.util.function패키지의 함수형 인터페이스에는 추상메서드 외에도 디폴트 메서드와 static메서드가 정의되어 있다.

//Function
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
static <T> Function<T, T> identity()
​
//Predicate
default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> or(Predicate<? super T> other)
default Predicate<T> negate()
static<T> Predicate<T> isEqual(Object targetRef)

* 원래 Function인터페이스는 반드시 두개의 타입을 지정해 줘야하기 때문에, 두 타입이 같아도 Function 라고 쓸 수 없다. Function<T, T>라고 써야한다.

Function의 합성

두 람다식을 합성해서 새로운 람다식을 만들 수 있다.

img

/*
    문자열을 숫자로 변환하는 함수 f와 숫자를 2진 문자열로 변환하는 함수 g를
    andThen()으로 합성하여 새로운 함수 h 만들기
*/
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);   //s를 16진수로 인식
Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
//함수 h의 지네릭 타입이 <String, String>이므로 String을 입력받아서 String을 결과로 반환한다.
Function<String, String> h = f.andThen(g);
​
/*
    compose()를 이용해 두 함수를 반대의 순서로 합성
*/
Function<Integer, String> g = (i) -> Integer.toBinaryString(i); //i를 2진 문자열로 반환
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);   //s를 16진수로 인식해서 변환
//함수 h의 지네릭 타입이 <Integer, Integer>이다.
Function<Integer, Integer> h = f.compose(g);
​
/*
    identity()는 함수를 적용하기 이전과 이후가 동일한 항등 함수가 필요할 때 사용한다.
    이 함수를 람다식으로 표현하면 x -> x 이다.
*/
Function<String, String> f = x -> x;
//Function<String, String> f = Function.identity(); //위의 문장과 동일
​
System.out.println(f.apply("AAA")); //AAA가 그대로 출력됨

* 항등 함수는 함수에 x를 대입하면 결과가 x인 함수를 말한다. f(x) = x

Predicate의 결합

여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.

Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;
Predicate<Integer> notP = p.negate();   //i >= 100
​
Predicate<Integer> all = notP.and(q).or(r); //100 <= i && i < 200 || i % 2 == 0
System.out.println(all.test(150);   //true
​
//람다식을 직접 넣기
Predicate<Integer> all = notP.and(i -> i < 200).or(i -> i % 2 == 0);

* Predicate의 끝에 negate()를 붙이면 조건식 전체가 부정이 된다.

static메서드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용한다.

먼저, isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정한다.

Predicate<String> p = Predicate.isEqual(str1);
boolean result = p.test(str2);  //str1과 str2가 같은지 비교하여 결과를 반환
​
//위 두 문장 합친 하나의 문장
boolean result = Predicate.isEqual(str1).test(str2);    //str1과 str2가 같은지 비교

8.메서드 참조

람다식이 하나의 메서드만 호출하는 경우에는 메서드 참조(method reference) 라는 방법으로 람다식을 간략히 할 수 있다.

//문자열을 정수로 변환하는 람다식
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
​
//메서드
Integer wrapper(String s) { //이 메서드의 이름은 의미없다.
    return Integer.parsetInt(s);
}
​
//메서드를 빼고 Integer.parseInt()를 직접 호출
//람다식의 일부가 생략되었지만,
//컴파일러는 생략된 부분을 우변의 parseInt메서드의 선언부로부터,
//또는 좌변의 Function인터페이스에 지정된 지네릭 타입으로부터 쉽게 알아낼 수 있다.
Function<String, Integer> f = Integer::parseInt;    //메서드 참조
​
//another Example
BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);
​
//참조변수 f의 타입을 봤을 때 람다식이 두 개의 String타입의 매개변수를 받는다는 것을 알 수 있다.
//그러므로, 람다식의 매개변수들은 없어도 된다.
//매개변수 s1과 s2를 생략하면 equals만 남는데,
//두 개의 String을 받아서 Boolean을 반환하는 equals라는 이름의 메서드는 다른 클래스에도
//존재할 수 있기 때문에 equals앞에 클래스 이름은 반드시 필요하다.
BiFunction<String, String, Boolean> f = String::equals; //메서드 참조
​
//이미 생성된 객체의 메서드를 람다식에서 사용한 경우에는
//클래스 이름 대신 그 객체의 참조변수를 적어줘야 한다.
MyClass obj = new MyClass();
Function<String, Boolean> f = (x) -> obj.equals(x); //람다식
Function<String, Boolean> f2 = obj::equals; //메서드 참조

람다식을 메서드 참조로 변환하는 방법

  • static메서드 참조 | (x) -> ClassName.method(x) | ClassName::method

  • 인스턴스메서드 참조 | (obj, x) -> obj.method(x) | ClassName::method

  • 특정 객체 인스턴스메서드 참조 | (x) -> obj.method(x) | obj::method

  • 하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름' 으로 바꿀 수 있다.

생성자의 메서드 참조

생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.

Supplier<MyClass> s = () -> new MyClass();  //람다식
Supplier<MyClass> s = MyClass::new; //메서드 참조

매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.

Function<Integer, MyClass> f = (i) -> new MyClass(i);   //람다식
Function<Integer, MyClass> f2 = MyClass::new;   //메서드 참조
​
BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);  //람다식
BiFunction<Integer, String, MyClass> bf2 = MyClass::new;    //메서드 참조
​
//배열 생성
Function<Integer, int[]> f = x -> new int[x];   //람다식
Function<Integer, int[]> f2 = int[]::new;   //메서드 참조 

메서드 참조는 람다식을 마치 static변수처럼 다룰 수 있게 해준다.

4.4 네 번째 과제! (진도표 4일차)

어떤 관점에서 접근했는지 : 강의 중심으로 정리

문제를 해결하는 과정은 무엇이었는지 : 아래에 정리

왜 그런 식으로 해결했는지 : 강의에서 벗어난 방식으로 구현하고 싶지 않았음

진도표 4일차와 연결됩니다 우리는 GET API와 POST API를 만드는 방법을 배웠습니다. 👍 추가적인 API 들을 만들어 보며 API 개발에 익숙해져 봅시다!

문제는 깃허브 프로젝트에서 확인 가능합니다. 코드는 이슈 #31에서도 확인이 가능합니다.


한 걸음 더! (문제 3번을 모두 푸셨다면) SQL의 sum, group by 키워드를 검색해 적용해보세요! :)

아래 컨트롤러는 sum, group by 키워드만 적용하여 구현한 코드를 포함하고 있습니다.

(학습 범위를 벗어난 기능은 사용하지 않습니다.)

지금까지 학습한 내용을 바탕으로 sum, group by 미포함된 문제3의 코드도 아래에 있으니 참고 바랍니다.

Task04ExController

package com.group.libarayapp.controller.task04;
​
import com.group.libarayapp.dto.task04.request.Task04CreateRequest;
import com.group.libarayapp.dto.task04.request.Task04ExRequest;
import com.group.libarayapp.dto.task04.response.Task04ExResponse;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
​
import java.util.ArrayList;
import java.util.List;
​
@RestController
public class Task04ExController {
​
    private final JdbcTemplate jdbcTemplate;
​
    public Task04ExController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
​
    // 문제 1
    @PostMapping("/api/v1/fruit")
    public void saveFruit(@RequestBody Task04ExRequest request) {
        String sql = "insert into fruit(name, warehousing, price)  values(?, ?, ?)";
        jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
    }
​
​
    // 문제 2
    @PutMapping("/api/v1/fruit")
    public void salesQuantityFruit(@RequestBody Task04ExRequest request) {
        String readSql = "select * from fruit where id=?";
        boolean isSalesFruit = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty();
        if (isSalesFruit) {
            throw new IllegalArgumentException();
        }
        String sql = "update fruit set salesQuantity=? where id=?";
        jdbcTemplate.update(sql, request.getSalesQuantity() + 1, request.getId());
    }
​
    // 문제 3 - sum, group by 적용
    @GetMapping("/api/v1/fruit/stat")
    public Task04ExResponse SalesAmountFruit(@RequestParam String name) {
        String readNotSalesSql = """
                select sum(price) as notSalesAmount
                from fruit
                where name=? and salesQuantity = 0
                group by salesQuantity;
                """;
        List<Integer> readNotSalesList = jdbcTemplate.query(readNotSalesSql, (rs, rowNum) -> rs.getInt("notSalesAmount"), name);
​
        String readSalesSql = """
                select sum(price) as salesAmount
                from fruit
                where name=? and salesQuantity = 1
                group by salesQuantity;
                """;
        List<Integer> readSalesList = jdbcTemplate.query(readSalesSql, (rs, rowNum) -> rs.getInt("salesAmount"), name);
​
        return new Task04ExResponse(readSalesList, readNotSalesList);
    }
}

Task04ExController - 문제3 - sum(), group by() 미적용으로 작성하기

package com.group.libarayapp.controller.task04;
...
​
@RestController
public class Task04ExController {
   ...
​
    private final List<Task04CreateRequest> Task04CreateRequestList = new ArrayList<>();
    long salesSum = 0;
    long notSalesSum = 0;
​
    // 문제 3 - sum, group by 미적용
    @GetMapping("/api/v1/fruit/stat")
    public Task04ExResponse SalesAmountFruit(@RequestParam String name) {
​
        // 모든 값을 List 에 저장
        String readSql = "select * from fruit";
        jdbcTemplate.query(readSql, (rs, rowNum) -> {
            Task04CreateRequest createRequest = new Task04CreateRequest(
                    rs.getLong("id"),
                    rs.getString("name"),
                    rs.getDate("warehousing").toLocalDate(),
                    rs.getLong("price"),
                    rs.getInt("salesQuantity"));
            Task04CreateRequestList.add(createRequest);
​
            return createRequest;
        });
​
        // 판매된 상품과 판매되지 않은 상품의 가격을 반환
        for (Task04CreateRequest task04CreateRequest : Task04CreateRequestList) {
            if (task04CreateRequest.getName().equals(name)) {
                if (task04CreateRequest.getSalesQuantity() > 0) {
                    salesSum += task04CreateRequest.getSalesQuantity() * task04CreateRequest.getPrice();
                } else {
                    notSalesSum += task04CreateRequest.getPrice();
                }
            }
        }
        return new Task04ExResponse(salesSum, notSalesSum);
    }
}

Task04ExResponse - 문제3 - sum(), group by() 미적용으로 작성하기

package com.group.libarayapp.dto.task04.response;
​
public class Task04ExResponse {
    private long salesAmount;
    private long notSalesAmount;
​
    public Task04ExResponse(long salesAmount, long notSalesAmount) {
        this.salesAmount = salesAmount;
        this.notSalesAmount = notSalesAmount;
    }
​
    public long getSalesAmount() {
        return salesAmount;
    }
​
    public long getNotSalesAmount() {
        return notSalesAmount;
    }
}

Task04CreateRequest

package com.group.libarayapp.dto.task04.request;
​
import java.time.LocalDate;
​
public class Task04CreateRequest {
​
    Long id;
    String name;
    LocalDate warehousing;
    Long price;
    int salesQuantity;
​
    public Task04CreateRequest(Long id, String name, LocalDate warehousing, Long price, int salesQuantity) {
        this.id = id;
        this.name = name;
        this.warehousing = warehousing;
        this.price = price;
        this.salesQuantity = salesQuantity;
    }
​
    public String getName() {
        return name;
    }
​
    public Long getId() {
        return id;
    }
​
    public LocalDate getWarehousing() {
        return warehousing;
    }
​
    public Long getPrice() {
        return price;
    }
​
    public int getSalesQuantity() {
        return salesQuantity;
    }
}

Task04ExRequest

package com.group.libarayapp.dto.task04.request;
​
import java.time.LocalDate;
​
public class Task04ExRequest {
​
    private Long id;
    private String name;
    private LocalDate warehousingDate;
​
    private int salesQuantity;
    private Long price;
​
    public Task04ExRequest(String name, LocalDate warehousingDate, Long price) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
    }
​
    public String getName() {
        return name;
    }
​
    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }
​
    public Long getPrice() {
        return price;
    }
​
    public Long getId() {
        return id;
    }
​
    public int getSalesQuantity() {
        return salesQuantity;
    }
}

Task04ExResponse

package com.group.libarayapp.dto.task04.response;
​
import java.util.List;
​
public class Task04ExResponse {
    private final long salesAmount;
    private final long notSalesAmount;
​
    public Task04ExResponse(List<Integer> readSalesList, List<Integer> readNotSalesList) {
        this.salesAmount = readSalesList.get(0);
        this.notSalesAmount = readNotSalesList.get(0);
    }
​
    public long getSalesAmount() {
        return salesAmount;
    }
​
    public long getNotSalesAmount() {
        return notSalesAmount;
    }
}


문제1

image

POST 방식으로 JSON 데이터인 이름, 날짜, 가격 을 MySQL에 저장하기 위해 update 쿼리문을 생성한다.

  1. 포스트맨으로부터 보낸 JSON 데이터는 @RequestBody 에 의해 이름, 날짜, 가격 을 가져오고 Task04ExRequest dto 에 매핑한다.

  2. jdbcTemplate.update() 메서드의 매개변수는 update 쿼리문 그리고 인 파라미터를 넣어준다. 인 파라미터는 매핑된 dto 클래스로부터 이름, 날짜, 가격을 getter 메서드로 불러오면 된다.

한 걸음 더! 자바에서 정수를 다루는 가장 대표적인 두 가지 방법은 intlong입니다. 이 두 가지 방법 중 위 API 에서 long을 사용한 이유는 무엇일까요?

int 보다 long이 더 많은 정수를 표현할 수 있기 때문입니다. 다음은 각 타입의 표현 범위입니다.

  • int 4바이트 : -2,147,483,648 ~ 2,147,483,647

  • long 8바이트 : -263 ~ (263 - 1), -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807


문제 2

image

과일이 판매되면 salesQuantity속성의 값이 1씩 증가하도록 변경했다. (판매된 갯수만큼 증가시키면 좋겠지만 판매 여부가 관건인 문제이다.) 판매되면 1, 판매가 안된 상황이면 0이다. 따라서salesQuantity 속성의 변경은 PUT 방식으로 작성하고 JSON 형식으로 id 를 요청하고 JSON 형식으로 id 를 응답 받는다 .


문제 3 - sum(), group by() 미적용 상태

image

데이터베이스에 저장된 모든 데이터를 리스트에 저장하고, 쿼리 파라미터와 동일한 과일의 판매 여부를 비교하여 판매된 과일 금액, 팔리지 않은 금액을 JSON 형식으로 응답한다.

문제 3 sum(), group by() 로 변경하기

image

주어진 문제 구하는 방식을 쿼리를 중심으로 구하도록 변경했다. sum(), group by() 쿼리문을 사용하여 요구사항 이외의 다른 문법을 배제하고 코드가 더욱 간결하고 적절하게 쿼리가 수행됐다.

4.5 다섯 번째 과제! (진도표 5일차)

어떤 관점에서 접근했는지 : 주사위 굴리는 코드를 클린 코드를 지향하고 최적화를 목표

문제를 해결하는 과정은 무엇이었는지 : 아래에 정리

왜 그런 식으로 해결했는지 : 클린 코드와 최적화가 주 목적이기 때문에 오히려 불필요한 클래스 분할은 무의미하다고 생각하여 다음과 같이 구현을 진행. 제품 및 서비스 관점에서 구현이 주 목표가 아닌 요구 사항에 따라 코드를 클린 코드를 지향하며 최적화에 중점적으로 구현하기 위해 실천.

전체 코드

package com.group.libarayapp.exercise.day05;
​
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;
​
public class DiceRollStatistics {
    private static final int NUMBER_OF_SIDES = 6;
    private static final int MAX_THREADS = Runtime.getRuntime().availableProcessors();
​
    public static void main(String[] args) {
        try (Scanner scanner = new Scanner(System.in)) {
            // 사용자로부터 주사위를 던질 횟수를 입력받습니다.
            System.out.print("주사위를 던질 횟수를 입력하세요: ");
            int numberOfThrows = scanner.nextInt();
​
            // 주사위를 던져 각 눈의 개수를 계산합니다.
            int[] diceCounts = getDiceCounts(numberOfThrows);
​
            // 통계를 출력합니다.
            printStatistics(diceCounts);
        }
    }
​
    private static int[] getDiceCounts(int numberOfThrows) {
        // 각 주사위 눈의 개수를 저장할 AtomicInteger 배열을 생성합니다.
        AtomicInteger[] counts = new AtomicInteger[NUMBER_OF_SIDES];
​
        // AtomicInteger 배열을 초기화합니다.
        IntStream.range(0, NUMBER_OF_SIDES)
                .forEach(i -> counts[i] = new AtomicInteger());
​
        // 주사위를 병렬로 던지고 각 눈의 개수를 계산합니다.
        IntStream.range(0, numberOfThrows)
                .parallel()
                .limit(MAX_THREADS) // 병렬 스트림에서 사용할 최대 스레드 수를 제한합니다.
                .forEach(i -> {
                    // 랜덤한 주사위 결과를 얻어서 해당하는 눈의 개수를 증가시킵니다.
                    int result = ThreadLocalRandom.current().nextInt(NUMBER_OF_SIDES);
                    counts[result].incrementAndGet();
                });
​
        // 각 눈의 개수를 배열로 변환하여 반환합니다.
        return IntStream.range(0, NUMBER_OF_SIDES)
                .map(i -> counts[i].get())
                .toArray();
    }
​
    private static void printStatistics(int[] diceCounts) {
        // 각 눈의 개수를 출력합니다.
        IntStream.range(0, NUMBER_OF_SIDES)
                .forEach(i -> System.out.printf("주사위 눈 %d은(는) %d번 나왔습니다.\n", i + 1, diceCounts[i]));
    }
}

코드 동작 요약

  1. java.util.Random 대신 java.util.concurrent.ThreadLocalRandom을 사용하여 더 효율적인 랜덤 생성을 수행합니다.

  2. try-with-resources 구문으로 Scanner를 자동으로 닫습니다.

  3. 스레드 안전한 AtomicInteger 배열을 사용하여 각 주사위 눈의 개수를 계산합니다.

  4. 병렬 스트림에서 최대 스레드 수를 제한하여 너무 많은 스레드가 생성되는 것을 방지합니다.

다음은 코드 라인을 기준으로 각 코드마다 작성한 이유에 대한 설명입니다.

private static final int NUMBER_OF_SIDES = 6;
private static final int MAX_THREADS = Runtime.getRuntime().availableProcessors();
  • NUMBER_OF_SIDES : 전체 주사위 크기를 6 으로 고정합니다.

  • MAX_THREADS : 병렬 스트림이 사용할 수 있는 최대 스레드 수를 제한합니다.

Runtime 클래스는 Java 어플리케이션과 JVM(Java Virtual Machine) 사이의 인터페이스를 제공하는 클래스입니다. 이 클래스의 인스턴스는 getRuntime() 메서드를 통해 얻을 수 있으며, JVM의 실행 환경에 대한 정보를 제공하고 시스템 리소스에 대한 액세스를 제어합니다.

멀티스레드 환경에서 병렬 작업을 수행을 위해 현재 내 컴퓨터의 실행 환경 정보 제공을 위해 availableProcessors() 메서드를 용하여 현재 시스템에서 사용 가능한 프로세서 수를 확인할 수 있습니다. availableProcessors() 메서드 뿐만 아니라 메모리 관리(freeMemory(), totalMemory()), 외부 프로세스 실행(exec()), 가비지 컬렉션(gc()), 시스템 종료(exit()) 등 다양한 기능을 지원하는 Runtime 클래스입니다.

현재 코드 내에서 Runtime 클래스 사용 목적은 병렬 스트림에서 사용할 최대 스레드 수를 최적화합니다.

public static void main(String[] args) {
    try (Scanner scanner = new Scanner(System.in)) {
        // 사용자로부터 주사위를 던질 횟수를 입력받습니다.
        System.out.print("주사위를 던질 횟수를 입력하세요: ");
        int numberOfThrows = scanner.nextInt();
​
        // 주사위를 던져 각 눈의 개수를 계산합니다.
        int[] diceCounts = getDiceCounts(numberOfThrows);
​
        // 통계를 출력합니다.
        printStatistics(diceCounts);
    }
  • numberOfThrows : 콘솔창에서 사용자로부터 주사위 던지는 횟수를 입력받습니다.

  • diceCounts : numberOfThrows만큼 주사위를 굴립니다.

  • printStatistics : 주사위 던진 결과를 출력합니다.

Main 메서드 안에는 입력과 로직 처리를 함수로 구현하여 간결하게 작성했습니다.

병렬로 구현한 주사위

private static int[] getDiceCounts(int numberOfThrows) {
    // 각 주사위 눈의 개수를 저장할 AtomicInteger 배열을 생성합니다.
    AtomicInteger[] counts = new AtomicInteger[NUMBER_OF_SIDES];
​
    // AtomicInteger 배열을 초기화합니다.
    IntStream.range(0, NUMBER_OF_SIDES)
        .forEach(i -> counts[i] = new AtomicInteger());
​
    // 주사위를 병렬로 던지고 각 눈의 개수를 계산합니다.
    IntStream.range(0, numberOfThrows)
        .parallel()
        .limit(MAX_THREADS) // 병렬 스트림에서 사용할 최대 스레드 수를 제한합니다.
        .forEach(i -> {
            // 랜덤한 주사위 결과를 얻어서 해당하는 눈의 개수를 증가시킵니다.
            int result = ThreadLocalRandom.current().nextInt(NUMBER_OF_SIDES);
            counts[result].incrementAndGet();
        });
​
    // 각 눈의 개수를 배열로 변환하여 반환합니다.
    return IntStream.range(0, NUMBER_OF_SIDES)
        .map(i -> counts[i].get())
        .toArray();
}

  1. 병렬 처리 시 스레드 안전성을 보장하기 위해 AtomicInteger를 활용합니다.

    // 각 주사위 눈의 개수를 저장할 AtomicInteger 배열을 생성합니다.
    AtomicInteger[] counts = new AtomicInteger[NUMBER_OF_SIDES];

    AtomicInteger는 Java에서 제공하는 스레드 안전한 int 자료형을 갖고 있는 Wrapping 클래스입니다. 멀티스레드 환경에서 동시성을 보장하는 AtomicInteger 클래스는 원자적으로 연산을 수행하여 스레드 간의 경쟁 조건(Race Condition)을 방지합니다.

    경쟁 조건(Race Condition)이란 여러 프로세스 / 스레드가 동시에 같은 데이터를 조작할 때 타이밍이나 접근 순서에 따라 결과가 달라질 수 있는 상황을 의미합니다.

자바 동시성 그리고 AtomicInteger 클래스

선수 지식이 필요한 내용입니다. 자바 기본 문법 책 혹은 강의를 통해 학습하면서 스레드에 대해 기본적인 이해를 갖고 있어야 합니다.

동시성(Concurrency) 이란 한 CPU에서 동시에 여러 작업을 하는 것처럼 보이게 만드는 것입니다. 한 CPU에서 2개의 프로세스가 있다고 가정합니다. 이 둘은 엄청나게 짧은 시간에 컨텍스트 스위치가 일어나며 번갈아 실행됩니다. 그래서 사람이 볼 때 동시에 동작하는 것처럼 보이는데, 이것이 프로그래밍 세계에서의 동시성입니다.

  • 하나의 코어에서 여러 쓰레드가 번갈아가며 실행하는 성질을 의미

  • 실제로는 한 코어에서 한 쓰레드만 실행하고 있고 번갈아가면서 실행하고 있기에 동시에 실행되는 것처럼 보이는 것

  • 쓰레드 개수가 코어 수보다 많으면 쓰레드 스케줄링을 통해 쓰레드를 어떤 순서로 동시성으로 실행할지 결정하는 게 필요

  • 자바는 우선순위 방식, 순환할당 방식으로 쓰레드 스케줄링을 구현

가시성(visibility)이란 값을 사용한 다음 블록을 빠져나가고 나면 다른 쓰레드가 변경된 값을 즉시 사용할 수 있게 해야 한다는 뜻입니다. 적절하게 동기화시키지 않으면 다른 쓰레드에서 최신 값이 아닌 예전 값을 읽게 됩니다. 따라서 가시성은 쓰레드가 항상 최신 값을 받아볼 수 있게 하는 성질입니다.

원자성(Atomicity)이란 어떤 것이 원자성을 갖고 있다면 원자적이라고 합니다. 어떤 작업이 실행될 때 언제나 완전하게 진행돼 종료되거나 그럴 수 없는 경우 실행을 하지 않는 경우를 말합니다. 원자성을 갖는 작업은 실행되어 진행되다가 종료하지 않고 중간에서 멈추는 경우는 있을 수 없습니다. 따라서 원자성은 어떤 작업이 프로그램(소스코드) 안에서 가장 작은 단위라서 더 이상 다른 작업으로 나눌 수 없는 성질입니다.

동시성 문제란?

스레드는 cpu 작업의 한단위입니다. 여기서 멀티스레드 방식은 멀티태스킹을 하는 방식 중, 한 코어에서 여러 스레드를 이용해서 번갈아 작업을 처리하는 방식입니다. 멀티 스레드를 이용하면 공유하는 영역이 많아 프로세스방식보다 context switcing(작업전환) 오버헤드가 작아, 메모리 리소스가 상대적으로 적다는 장점이 있습니다. 하지만 자원을 공유해서 단점도 존재합니다. 그것이 바로, 동시성(concurrency) 문제입니다. 여러 스레드가 동시에 하나의 자원을 공유하고 있기 때문에 같은 자원을 두고 경쟁 상태(Race Condition) 같은 문제가 발생하는 것입니다.

동시성 문제 해결의 세 가지 방법

자바에서 동시성 문제를 해결하는데 3가지 방법이 있습니다.

volatile 은 Thread1에서 쓰고, Thread2에서 읽는 경우만 동시성을 보장합니다. 두 개의 쓰레드에서 쓴다면 문제가 될 수 있습니다. synchronized를 쓰면 안전하게 동시성을 보장할 수 있습니다. 하지만 비용이 가장 큽니다. Atomic 클래스는 CAS(compare-and-swap)를 이용하여 동시성을 보장합니다. 여러 쓰레드에서 데이터를 write해도 문제가 없습니다. AtomicIntegersynchronized 보다 적은 비용으로 동시성을 보장할 수 있습니다.

AtomicInteger 객체 생성

AtomicInteger 클래스는 동기화를 사용하거나 성능에 큰 영향을 주지 않고 동시에 다른 스레드에서 액세스하는 정수 카운터에 대한 스레드로부터 안전한 작업을 제공합니다. AtomicInteger 클래스에는 모두 스레드로부터 안전한 많은 유틸리티 메서드가 있습니다. [오라클 자바 8 공식 홈페이지](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicInteger.html)를 통해 자바 8에서 추가된 메서드도 함께 확인할 수 있습니다.

다음은 AtomicInteger 객체 생성의 간단한 예제입니다.

public void atomicInteger1() {
    AtomicInteger atomic = new AtomicInteger();
    System.out.println("value : " + atomic.get());
​
    AtomicInteger atomic2 = new AtomicInteger(10);
    System.out.println("value : " + atomic2.get());
}

// 결과
value : 0
value : 10 

초기값은 0이며, 초기값을 변경하고 싶으면 인자로 int 변수를 전달하면 됩니다. AtomicInteger 메서드에 대한 사용 방법은 https://codechacha.com/ko/java-atomic-integer/ 블로그에서도 확인할 수 있습니다.

  1. AtomicInteger 배열을 생성하고 초기화하여 멀티 스레드 환경에서 안전하게 사용할 수 있도록 준비하는 역할을 합니다.

    // AtomicInteger 배열을 초기화합니다.
    IntStream.range(0, NUMBER_OF_SIDES)
        .forEach(i -> counts[i] = new AtomicInteger());

     

    IntStream.range(0, NUMBER_OF_SIDES)0부터 NUMBER_OF_SIDES - 1까지의 정수 스트림을 생성합니다. 즉, 주사위의 면의 수(NUMBER_OF_SIDES)에 해당하는 횟수만큼 반복됩니다.

    forEach(i -> counts[i] = new AtomicInteger())는 각 인덱스(i)에 대해 AtomicInteger 객체를 생성하여 AtomicInteger 배열(counts)에 할당합니다. 이렇게 함으로써 AtomicInteger 배열이 초기화되고, 각 원소는 스레드 안전한 정수형 변수로 사용될 수 있습니다.

  2. 병렬로 주사위를 던지고, 각 주사위 눈의 개수를 안전하게 카운트하는 작업을 수행합니다.

    // 주사위를 병렬로 던지고 각 눈의 개수를 계산합니다.
    IntStream.range(0, numberOfThrows)
        .parallel()
        .limit(MAX_THREADS) // 병렬 스트림에서 사용할 최대 스레드 수를 제한합니다.
        .forEach(i -> {
            // 랜덤한 주사위 결과를 얻어서 해당하는 눈의 개수를 증가시킵니다.
            int result = ThreadLocalRandom.current().nextInt(NUMBER_OF_SIDES);
            counts[result].incrementAndGet();
    });

     

    주어진 횟수만큼 주사위를 병렬로 던지고, 각 주사위 눈의 개수를 계산하는 부분입니다.

    1. IntStream.range(0, numberOfThrows)은 0부터 numberOfThrows - 1까지의 정수 스트림을 생성합니다. 이는 주어진 횟수(numberOfThrows)만큼 반복됩니다.

    2. parallel() 메서드는 병렬 스트림을 생성합니다. 이를 통해 주어진 범위의 요소들이 병렬로 처리됩니다.

    3. limit(MAX_THREADS)는 병렬 스트림에서 사용할 최대 스레드 수를 제한합니다. 이렇게 함으로써 너무 많은 스레드가 생성되는 것을 방지하고, 시스템 자원을 효율적으로 사용할 수 있습니다.

    4. forEach() 메서드는 각 요소에 대해 주어진 작업을 수행합니다. 여기서는 람다식으로 주사위를 던지고, 각 주사위 눈의 개수를 증가시키는 작업을 수행합니다.

    5. ThreadLocalRandom.current().nextInt(NUMBER_OF_SIDES)는 현재 스레드의 지역 랜덤 인스턴스에서 주어진 범위(NUMBER_OF_SIDES) 내에서 랜덤한 정수를 생성합니다. 즉, 0부터 NUMBER_OF_SIDES - 1까지의 랜덤한 숫자를 얻어옵니다.

    6. counts[result].incrementAndGet()는 주사위의 결과에 해당하는 인덱스(result)에 해당하는 AtomicInteger의 값을 1 증가시킵니다. 이를 통해 각 주사위 눈의 개수를 카운트할 수 있습니다.

  3. AtomicInteger 배열에 저장된 값을 가져와서 일반 정수 배열로 변환하여 반환하는 작업을 수행합니다.

// 각 눈의 개수를 배열로 변환하여 반환합니다.
return IntStream.range(0, NUMBER_OF_SIDES)
    .map(i -> counts[i].get())
    .toArray();
  1. IntStream.range(0, NUMBER_OF_SIDES)0부터 NUMBER_OF_SIDES - 1까지의 정수 스트림을 생성합니다. 이는 주사위의 눈의 개수(NUMBER_OF_SIDES)만큼 반복됩니다.

  2. map(i -> counts[i].get())는 각 인덱스(i)에 해당하는 AtomicInteger의 값을 가져와서 매핑합니다. 즉, AtomicInteger 배열에 저장된 값들을 가져와서 일반 정수 값으로 변환합니다.

  3. toArray()는 스트림에 있는 요소들을 배열로 변환하여 반환합니다. 이렇게 함으로써 AtomicInteger 배열에 저장된 각 주사위 눈의 개수를 포함한 일반 정수 배열을 생성합니다.

  4. 이렇게 생성된 배열은 주사위 눈의 개수를 나타내며, 이를 반환하여 주사위 눈의 통계를 완성합니다.

주사위 통계 출력

주어진 주사위 눈의 개수 배열을 받아서 각 눈의 개수를 형식에 맞게 출력하는 작업을 수행합니다.

private static void printStatistics(int[] diceCounts) {
    // 각 눈의 개수를 출력합니다.
    IntStream.range(0, NUMBER_OF_SIDES)
        .forEach(i -> System.out.printf("주사위 눈 %d은(는) %d번 나왔습니다.\n", i + 1, diceCounts[i]));
}
  1. IntStream.range(0, NUMBER_OF_SIDES)0부터 NUMBER_OF_SIDES - 1까지의 정수 스트림을 생성합니다. 즉, 주사위의 눈의 개수(NUMBER_OF_SIDES)만큼 반복됩니다.

  2. forEach(i -> System.out.printf("주사위 눈 %d은(는) %d번 나왔습니다.\n", i + 1, diceCounts[i]))는 각 인덱스(i)에 해당하는 주사위 눈의 개수를 가져와서 출력합니다. printf 메서드를 사용하여 주사위 눈의 번호와 해당하는 개수를 형식에 맞게 출력합니다. 여기서 i + 1은 눈의 번호를 1부터 시작하도록 보정하는 역할을 합니다.

  3. 이렇게 각 눈의 개수를 출력하고, 마지막에는 개행 문자(\n)를 추가하여 줄을 바꿔줍니다.


한 걸음 더!

  • 현재 코드는 주사위가 1부터6까지만 있다는 가정으로 작성되어 있습니다. 따라서 주사위가 1부터12까지 있거나 1부터20까지 있다면 코드를 많이 수정해야 합니다.

  • 위의 코드를 클린하게 개선해 보았다면, 주사위의 숫자 범위가 달라지더라도 코드를 적게 수정할 수 있도록 고민해 봅시다!

import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;
​
public class DiceRollStatistics {
    public static void main(String[] args) {
        try (Scanner scanner = new Scanner(System.in)) {
            System.out.print("주사위의 최소 숫자를 입력하세요: ");
            int minNumber = scanner.nextInt();
​
            System.out.print("주사위의 최대 숫자를 입력하세요: ");
            int maxNumber = scanner.nextInt();
​
            int numberOfSides = maxNumber - minNumber + 1;
​
            // 사용자로부터 주사위를 던질 횟수를 입력받습니다.
            System.out.print("주사위를 던질 횟수를 입력하세요: ");
            int numberOfThrows = scanner.nextInt();
​
            // 주사위를 던져 각 눈의 개수를 계산합니다.
            int[] diceCounts = getDiceCounts(numberOfThrows, numberOfSides);
​
            // 통계를 출력합니다.
            printStatistics(diceCounts);
        }
    }
​
    private static int[] getDiceCounts(int numberOfThrows, int numberOfSides) {
        // 각 주사위 눈의 개수를 저장할 AtomicInteger 배열을 생성합니다.
        AtomicInteger[] counts = new AtomicInteger[numberOfSides];
​
        // AtomicInteger 배열을 초기화합니다.
        IntStream.range(0, numberOfSides)
                .forEach(i -> counts[i] = new AtomicInteger());
​
        // 주사위를 병렬로 던지고 각 눈의 개수를 계산합니다.
        IntStream.range(0, numberOfThrows)
                .parallel()
                .forEach(i -> {
                    // 랜덤한 주사위 결과를 얻어서 해당하는 눈의 개수를 증가시킵니다.
                    int result = ThreadLocalRandom.current().nextInt(numberOfSides);
                    counts[result].incrementAndGet();
                });
​
        // 각 눈의 개수를 배열로 변환하여 반환합니다.
        return IntStream.range(0, numberOfSides)
                .map(i -> counts[i].get())
                .toArray();
    }
​
    private static void printStatistics(int[] diceCounts) {
        // 각 눈의 개수를 출력합니다.
        IntStream.range(0, diceCounts.length)
                .forEach(i -> System.out.printf("주사위 눈 %d은(는) %d번 나왔습니다.\n", i + 1, diceCounts[i]));
    }
}

댓글을 작성해보세요.