728x90

4일차에 만들었던 API를 Controller - Service - Repository로 나누어 보았다.

4일차 과제 보기 👉 https://ddonydev.tistory.com/74  

 

  • Controller
@RestController
public class FruitController {

    private final FruitService fruitService;

    public FruitController(FruitService fruitService) {
        this.fruitService = fruitService;
    }

    @GetMapping("/api/v1/fruit/stat")
    public FruitResponse selectFruits(@RequestParam String name) {
        return fruitService.selectFruits(name);
    }


    @PostMapping("/api/v1/fruit")
    public void insertFruits(@RequestBody FruitRequest fruitRequest) {
        fruitService.insertFruits(fruitRequest);
    }

    @PutMapping("/api/v1/fruit")
    public void updateFruits(@RequestBody FruitUpdateRequest request) {
        fruitService.updateFruits(request);
    }
}

 

  • Service
@Service
public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    public FruitResponse selectFruits(String name) {
        return fruitRepository.selectFruits(name);
    }

    public void insertFruits(FruitRequest fruitRequest){
        fruitRepository.insertFruits(fruitRequest.getName(), fruitRequest.getWarehousingDate(), fruitRequest.getPrice());
    }

    public void updateFruits(FruitUpdateRequest request) {

        boolean isExist = fruitRepository.isExist(request.getId());

        if (isExist) {
            throw new RuntimeException("존재 하지 않는 과일입니다.");
        }

        fruitRepository.updateFruits(request.getId());

    }
}

 

  • Repository
@Repository
public class FruitRepository {

    private final JdbcTemplate jdbcTemplate;

    public FruitRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public FruitResponse selectFruits(String name) {
        String sale = "SELECT SUM(price) as salesAmount FROM fruits WHERE isSold = true AND name = ?";
        String notSale = "SELECT SUM(price) as notSalesAmount FROM fruits WHERE isSold = false AND name = ?";

        long salesAmount = jdbcTemplate.queryForObject(sale, (rs, rowNum) -> rs.getLong("salesAmount"), name);
        long notSalesAmount = jdbcTemplate.queryForObject(notSale, (rs, rowNum) -> rs.getLong("notSalesAmount"), name);

        return new FruitResponse(salesAmount, notSalesAmount);
    }

    public void insertFruits(String name, LocalDate warehousingDate, long price) {
        String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, name, warehousingDate, price);
    }

    public boolean isExist(long id) {
        String readSql = "SELECT * FROM fruits WHERE ID = ?";
        return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
    }

    public void updateFruits(long id) {
        String updateSql = "UPDATE fruits SET isSold = true where id = ?";
        jdbcTemplate.update(updateSql, id);
    }

}

 

전 코드에서는 Controller에서 SQL 처리까지 다 해주었는데 Service에서는  isExist와 같이 "존재하는지 판별하기" 등 주요 비즈니스 처리를 해주었고, Repository에서는 DB에 접근 할 수 있도록 역할을 나누었다. 그리고 이전 코드에서는 Controller에서 JDBCTemplate를 주입받아 사용하였는데 이번 코드에서는 Repository에서 JDBCTemaplate를 주입받아 DB와 통신하고 Service, Controller에는 결과를 return해주었다.

 

 

  • Interface
public interface FruitRepository {

    FruitResponse selectFruits(String name);

    void insertFruits(String name, LocalDate warehousingDate, long price);

    boolean isExist(long id);

    void updateFruits(long id);

}

 

  • FruitMemoryRepository
@Repository
public class FruitMemoryRepository implements FruitRepository{
    private final JdbcTemplate jdbcTemplate;

    public FruitMemoryRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public FruitResponse selectFruits(String name) {
        String sale = "SELECT SUM(price) as salesAmount FROM fruits WHERE isSold = true AND name = ?";
        String notSale = "SELECT SUM(price) as notSalesAmount FROM fruits WHERE isSold = false AND name = ?";

        long salesAmount = jdbcTemplate.queryForObject(sale, (rs, rowNum) -> rs.getLong("salesAmount"), name);
        long notSalesAmount = jdbcTemplate.queryForObject(notSale, (rs, rowNum) -> rs.getLong("notSalesAmount"), name);

        return new FruitResponse(salesAmount, notSalesAmount);
    }

    @Override
    public void insertFruits(String name, LocalDate warehousingDate, long price) {
        String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, name, warehousingDate, price);
    }

    @Override
    public boolean isExist(long id) {
        String readSql = "SELECT * FROM fruits WHERE id = ?";
        return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
    }

    @Override
    public void updateFruits(long id) {
        String updateSql = "UPDATE fruits SET isSold = true WHERE id = ?";
        jdbcTemplate.update(updateSql, id);
    }
}

 

  • FruitMysqlRepository
@Repository
@Primary
public class FruitMySqlRepository implements FruitRepository{
    private final JdbcTemplate jdbcTemplate;

    public FruitMySqlRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public FruitResponse selectFruits(String name) {
        String sale = "SELECT SUM(price) as salesAmount FROM fruits WHERE isSold = true AND name = ?";
        String notSale = "SELECT SUM(price) as notSalesAmount FROM fruits WHERE isSold = false AND name = ?";

        long salesAmount = jdbcTemplate.queryForObject(sale, (rs, rowNum) -> rs.getLong("salesAmount"), name);
        long notSalesAmount = jdbcTemplate.queryForObject(notSale, (rs, rowNum) -> rs.getLong("notSalesAmount"), name);

        return new FruitResponse(salesAmount, notSalesAmount);
    }

    @Override
    public void insertFruits(String name, LocalDate warehousingDate, long price) {
        String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, name, warehousingDate, price);
    }

    @Override
    public boolean isExist(long id) {
        String readSql = "SELECT * FROM fruits WHERE id = ?";
        return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
    }

    @Override
    public void updateFruits(long id) {
        String updateSql = "UPDATE fruits SET isSold = true WHERE id = ?";
        jdbcTemplate.update(updateSql, id);
    }
}

 

이렇게 위의 요구사항과 같이 Interface로 나누고 FruitMemoryRepositoryFruitMySqlRepositoryFruitRepository를 구현하도록 하였다. 이렇게 되면 Service에 코드 수정 없이 RepositoryMemory로 사용할지 Mysql로 사용할지 사용할 수 있다.

 

💡그렇다면 어떻게 사용할까?

 

현재 FruitMySqlRepository를 보면 @Primary 어노테이션이 있다. 이 어노테이션은 "FruitMySqlRepository을 사용할거야~"라고 알려주는 어노테이션이다. 만약 @Primary가 없다면 Service 코드의 생성자 부분에서 오류가 나게된다.

 

다음으론 @Qualifier어노테이션이 있다. @Qualifier("fruitMemoryRepository")Service 코드의 생성자 부분에 사용하면 FruitMySqlRepository @Primary가 붙어 있더라도 fruitMemoryRepository를 사용하게 된다.

(스프링에서는 사용자가 직접 적어준 @Qualifier가 우선순위를 가진다.)

@Service
public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService
    (@Qualifier("fruitMemeoryRepository")FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    // 이하 생략
}

 

@Qualifier 어노테이션은 @Qualifier("fruitMemoryRepository") 이런 방식이 아니어도 아래와 같은 방식으로도 사용할 수 있다.

// 서비스 코드
@Service
public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService
    (@Qualifier("fruit")FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    // 이하 생략
}

// 레포지토리 코드
@Repository
@Qualifier("fruit")
public class FruitMemoryRepository implements FruitRepository{
    // 이하 생략
}

 

사용하려는 곳에 직접 이름을 지정해주고, 지정해준 이름을 생성자 앞에 적어주는 방식으로도 사용할 수 있다.

 

@Qualifier를 사용시 클래스 이름을 그대로 적어주면 아래와 같이 오류가 났다.

Inspection 'Incorrect autowiring in Spring bean components' has no quick-fixes for this problem.
Click to edit inspection options, suppress the warning, or disable the inspection completely.

 

이는 Spring 프레임워크에서 사용되는 빈(Bean)에 대한 자동 와이어링(Autowiring) 설정이 올바르지 않을 때 발생하는 경고이다. 경고가 "quick-fixes"를 제공하지 않는다는 것은 IDE가 자동으로 이 문제를 해결할 수 있는 방법이 없다는 것을 의미한다. 보통은 해당 빈을 명시적으로 주입하도록 코드를 수정하거나, 인스펙션 설정을 변경하여 경고를 무시하거나 비활성화하는 등의 수동 조치가 필요하다.

라고 chat-gpt가 이야기 해주었다..

 

@Autowired 어노테이션을 추가하거나 IntelliJSettings 메뉴에서 Editor > Inspections로 이동하여 "Incorrect autowiring in Spring bean components" 검색하여 해당 인스펙션의 옵션을 수정하라고 하였다.

 

자세히 강의를 보니 강사님께서 클래스 이름을 FruitMemoryRepository로 작성하셨는데 첫 글자를 소문자로 작성하신것을 보고 바꾸니까 에러가 없어졌다.

 

왜 그런지 아직 잘 모르겠다.. 조금 찾아보아야겠다.

 

[이유를 알아냈다..!]

@Qualifier는 직접 빈으로 등록해준 것이 아니라면 빈으로 설정된 Default 이름을 적어주어야한다.

 

FruitMemoryRepository의 등록된 이름을 확인해보니 아래와 같았다.

 

즉, Default 이름은 첫 글자가 소문자인 이름으로 등록되는 것이었다.

요것도 몰랐다니..아무튼 이번에 알게 되었으니 꼭 기억해야겠다!

728x90
728x90

나는 제일 많이 사용하는 Ubuntu를 사용하여 VM을 만들었다.

Ubuntu 공식 홈페이지를 클릭하면 다운로드 페이지로 들어갈 수 있다.

Download Ubuntu Server 22.04.4 LTS를 클릭하여 다운 받을 수 있다.

 

다운로드를 하고 이 파일을 Proxmox 웹 관리 페이지로 가서 넣어주어야한다.

 

 

Proxmox에서 local(pve)를 누른다.

 

 

누르게 되면 ISO Images라는 탭이 있고 여기서 Uplad라는 버튼을 올려

아까 다운받은 Ubuntu Server 파일을 올려주면 된다.

 

파일을 올리고 나면 오른쪽 상단에 있는 Create VM을 눌러준다.

 

VM 아이디와 이름을 적어준뒤 다음을 눌러준다.

 

OS는 아까 올려준 Ubuntu를 선택해준다.

 

기본 바이오스를 사용할 것이기 때문에 다음을 눌러준다.

 

디스크 사이즈를 정해준다. 나는 50GB로 했다.

그리고 여기서 Cache를 Wite back으로 한다. (이게 더 안정성 있다고..)

 

CPU의 코어수를 정해준다.

 

메모리의 크기를 정해준다.

 

다음을 눌러준다.

 

이러면 VM생성이 완료 된다!

728x90

'서버' 카테고리의 다른 글

개인 홈 서버 - Proxmox 설치하기  (1) 2024.02.26
개인 홈 서버 구축하기  (0) 2024.01.31
728x90

1. Proxmox란?

Proxmox는 가상화 및 컨테이너 기술을 기반으로 한 오픈 소스 플랫폼이다.

  • 가상화: Proxmox는 KVM (Kernel-based Virtual Machine) 및 LXC (Linux Containers)와 같은 가상화 기술을 지원하여 가상 머신 및 컨테이너를 관리할 수 있다.
  • 웹 기반 관리: Proxmox는 웹 기반 관리 인터페이스를 제공하여 VM 및 컨테이너, 클러스터의 고가용성 또는 통합 재해 복구 도구를 쉽게 관리할 수 있다.
  • 저장 및 백업: 데이터 스토리지 및 백업 솔루션을 통합하여 데이터의 안전한 보호와 관리를 지원한다.

가상화 플랫폼에는 Proxmox 외에도 ESXi(VMware ESXi)가 있다.

 

2. proxmox 사용 이유

서버는 기본적으로 리눅스를 기반으로 돌아간다.

단순히 서버를 사용할 것이라면 Ubuntu와 같은 서버를 사용해도 되지만 서버 하나에 한 프로그램만 동작하기 때문에 비효율 적이다.

여러 개의 프로그램을 운영하기 위해 사용하는 것이 가상 머신(VirtualBox)인데 이를 하이퍼바이저에 올려 작동 시킬 수 있다.

 

💡그렇다면 하이퍼바이저(Hypervisor)란 무엇일까?

하이퍼바이저란 대부분의 컴퓨터는 하나의 운영체제(OS)만 실행할 수 있다. 하드웨어는 하나의 운영체제만 처리하면 되기 때문에 안정적이지만 컴퓨터의 모든 전력을 사용한다는 단점이 있다.

이를 해결해준 것이 하이퍼바이저이다. 하이퍼바이저는 여러 인스턴스가 동일한 물리적 컴퓨팅 리소스를 공유하여 동시에 실행 할 수 있도록 해준다. 이를 가상화라고 하는데 하나의 물리적인 리소스(예: 컴퓨터, 서버, 스토리지 등)를 여러 개의 가상 리소스로 분할하여 사용하는 기술이다.
하이퍼바이저는 가상머신 모니터라고도 불리는데 이 가상 머신을 관리할때 사용한다.

하이퍼바이저에는 Type 1과 Type2가 있다.

1. Type1
네이티브 Hypervisor로, 호스트 시스템의 운영체제 위에 직접 실행된다.
이러한 Hypervisor는 높은 성능과 안정성을 제공하지만, 호스트 시스템과의 호환성 문제가 발생할 수 있다.
(대표적으로 EXSi, Proxmox 등이 있다.)

2. Type2
호스트형 Hypervisor로, 호스트 운영체제 위에서 실행된다.
이러한 Hypervisor는 호스트 시스템과의 호환성이 높고, 사용하기 쉽지만,
성능 면에서는 타입 1 Hypervisor보다는 떨어질 수 있다.
(대표적으로 Window, Linux 등에 올리는 가상화가 있다.)

 

정리하자면, 여러 개의 가상 머신을 관리하기 위해 사용되는 것이 하이퍼바이저인데 Proxmox는 하이퍼바이저이고 Type1에 속한다.

 

더 자세한 설명은 Proxmox VE에서 확인할 수 있다.

 

3. Proxmox 설치방법

먼저 Proxmox 공식 사이트로 들어가면 ISO 파일을 다운 받을 수 있다.

usb에 ISO파일을 다운 받아 부팅 디스크를 만들어 주었다.

 

아무런 OS도 깔려있지 않은 컴퓨터이기 때문에 따로 부팅 디스크를 잡아주지 않아도 usb를 넣으면 설치가 진행 된다.

처음 설치 화면이 나오고 넘기다 보면 나라 설정이 나오고 설정 후에 다음을 넘기다 보면

Proxmox 관리 페이지에 로그인할 Password설정과 이메일 입력 창이 나오고 입력을 해준다.

이후 Hostname과 IP Address(잘 못 만져서 처음에 오류가 생겼다.)와 DNS가 입력된 페이지가 나온다.

나는 IP 빼고는 건드리지 않았다.

다음 넘기다 보면 그동안 입력했던 정보들이 나오고 설치를 진행하면 된다.

 

설치가 끝나면 재부팅이 되고 웹 페이지에 접속할 IP가 나온다.(이때 IP를 꼭 기억하자..!)

 

설치가 완료되면 아래와 같은 화면이 나온다. 이후 로그인을 하고 사용하면 된다.

 

[IP Address를 만져서 생긴 오류😭]

아무 생각 없이 그냥 마음대로 내가 원하는 IP를 적었다.

이랬더니 설치 후 Proxmox 관리 페이지에 접속이 되지 않았고 확인해본 결과

공유기에서 DHCP에 할당 된 IP(컴퓨터에 할당된 IP)를 작성하거나(그대로 뒀어야)하는데 만져서 접속이 되지 않은 오류였다..!

 

// address 바꾸는 방법
nano etc/network/interfaces

auto vmbr0
iface vmbr0 inet static
        address 192.168.1.100/24 // -> 할당된 IP
        gateway // 기본 설정된 값
        bridge-ports enp1s0
        bridge-stp off
        bridge-fd 0
        
// 다시 시작
service networking restart
reboot

코드 참고

728x90

'서버' 카테고리의 다른 글

개인 홈 서버 - VM 만들기  (1) 2024.02.26
개인 홈 서버 구축하기  (0) 2024.01.31
728x90

 

 

[1주차 회고]

벌써 스터디를 진행한지 1주차가 되었다.

 

스터디 방식은 하루에 정해진 양의 강의를 듣고 주어진 과제를 정리하고 공부하는 것이다.

강의는 굉장히 쉽게 풀어서 설명해주셨고 이해가 쏙쏙 되었다.

하지만 막상 과제를 보고 정리하려고 하니 이해가 된 것은 맞는지..

머리 속은 엉망이 되었고 정리를 하다보면 2~3시간은 훌쩍 가 있었다.

 

그동안 다른 강의들을 들으면 다양한 방면으로 생각해보지 못하고 아 그렇구나~ 그런거구나하며 넘어갔던 적이 많았다.

하지만 강의를 듣고 과제를 하면서 "이건 왜 이런거지?", "이럴 땐 이렇게 되나? 이게 있으면 다른게 있지 않을까?" 등 생각의 넓이가 달라졌고,

다른 러너분들이 과제를 수행하신 것을 보고 "이런 방법도 있구나", "이런 생각도 하시는구나" 라며 다양한 의견을 볼 수 있었다.

 

그동안 혼자 공부하면서 남들과는 실력차이가 나 많이 좌절도 했고 어떻게 해야할지 많은 고민이 있었는데,

이번 기회로 다른 러너분들의 다양한 풀이 방법을 보고 최대한 습득하고 고민해보고 질문해보아야 겠다는 생각이 들었다.

 

좋은 강의를 제공해주신 최태현 강사님과 다른 분들과 소통할 수 있는 기회를 만들어주신 인프런 분들께 감사드립니다.

좋은 기회를 주신 만큼 열심히 공부하고 많은 것을 얻어가겠습니다..!

 

다음 한 주도 화이팅💪

 

 

 

강의 링크 👉 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

728x90
728x90

출처 - https://inf.run/XKQg

 

출처 - https://inf.run/XKQg

 

 

[작성 코드]

import java.util.Scanner;

public class Main {

    static int[] numArr;
    public static void main(String[] args) {

        int num = inputNum();
        numArr = new int[num];
        dice(num);
        print(numArr);
        
    }

    public static int inputNum() {
        Scanner scanner = new Scanner(System.in);
        System.out.print("숫자를 입력하세요 : ");
        return scanner.nextInt();
    }

    public static void dice(int num) {
        for (int i = 0; i < num; i++) {
            double random = Math.random() * num;
            numArr[(int) random - 1]++;
        }
    }

    public static void print(int[] numArr) {
        for (int i = 0; i < numArr.length; i++) {
            System.out.printf("%d는 %d번 나왔습니다.\n", i + 1, numArr[i]);
        }
    }
}

 

  • numArr은 주사위 던지기 결과를 저장하는 배열로 static 변수로 선언해준다.
  • 숫자를 입력 받는 inpunNum()메서드를 만들어 입력 받는다.
  • 입력 받은 숫자 만큼 numArr의 길이를 지정해준다.
  • dice 메서드를 만들어 입력한 숫자만큼 주사위를 던지고, 각 숫자가 몇번 나왔는지 numArr에 저장해준다.
  • print메서드로 각 숫자가 몇번 나왔는지 출력해준다.

 

[정리]

각 메서드에서 numArr를 사용할 수 있도록 static(전역)변수로 선언하였고,

주사위의 최대 숫자는 다를 수 있으므로 숫자 입력을 받아 num 변수에 대입하였다.

(입력받은 num 만큼 주사위 놀이를 할 수 있다.)

 

처음엔 print()메서드를 따로 빼지 않았는데 기능별로 더 세분화 시키는 것이 나을 것 같아서 분리하였다.

 

클린 코드에 대해 아직 공부를 미뤄 두었는데..

오늘 계기로 빠르게 읽고 정리해봐야겠다..!

 

 

 

 

강의 링크 👉 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

728x90
728x90

출처 -&nbsp;https://inf.run/XKQg

 

1. DB

// 데이터베이스 생성
create database test;

// 데이터베이스 사용
use test;

// 테이블 생성
create table fruits (
    id bigint auto_increment,
    name varchar(20) not null,
    warehousingDate date not null,
    price bigint not null,
    isSold boolean default false,
    primary key (id)
);

 

 

2. Controller 코드

@RestController
public class FruitController {
    private final JdbcTemplate jdbcTemplate;

    public FruitController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping("/api/v1/fruit")
    public void insertFruits(@RequestBody FruitRequest fruitRequest) {
        String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, fruitRequest.getName(), 
        fruitRequest.getWarehousingDate(), fruitRequest.getPrice());
    }
 }
  • 생성자를 사용해 JdbcTemplate 객체를 주입 받는다.
  • Post요청으로 RequestBody 어노테이션을 사용하여 HTTP body 에서 과일 데이터(FruitRequest)를 받아온다.
  • 과일에 대한 정보(이름, 등록 날짜, 가격)을 넣어준다.
  • JdbcTemplate의 update메서드를 사용하여 쿼리를 실행한다.

 

3. DTO 코드

import java.time.LocalDate;

public class FruitRequest {
    private long id;
    private String name;
    private LocalDate warehousingDate;
    private long price;

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public long getPrice() {
        return price;
    }
}

 

4. 실행 결과

 

 

[질문] - 위 API에서 long 타입을 사용하는 이유는?

자바에서 대표적으로 사용하는 정수형 타입엔 int와 long이 있다. (byte와 short 도 있음)
하지만 여기서 long을 사용하는 이유는 무엇일까?

int형 데이터 타입에 저장 될 수 있는 정수의 범위는 –2,147,483,648 ~ 2,147,483,647으로 약 -21억 ~ 21억이다.
하지만 long 타입은  -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,808이다.

int타입에 비하면 엄청나게 많은 숫자이다.
작은 프로그램을 만들때에는 int형 데이터 타입으로도 충분히 저장 가능하다.

하지만 은행과 같은 숫자를 많이 다루는 대형 프로그램에서는 int형 넘어가는 범위의 숫자가 잘못 저장되는 것을 방지하기 위해
또, 나중에 프로그램이 커질 것을 대비하여 int타입으로 사용하기 보다는 처음부터 long타입으로 지정 하는 것이다.
(나중에 다시 변환하려고 하면 머리 아프니까..)

그렇다고 무조건 long타입이 좋은 것은 아니다 프로젝트 규모에 따라 잘 설계하는 것이 가장 좋은 설계인 것 같다.

 

 

출처 -&nbsp;https://inf.run/XKQg

 

1. Controller 코드

@RestController
public class FruitController {
    private final JdbcTemplate jdbcTemplate;

    public FruitController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PutMapping("/api/v1/fruit")
    public void updateFruits(@RequestBody FruitUpdateRequest request) {
        String readSql = "SELECT * FROM fruits WHERE ID = ?";
        boolean isExist = jdbcTemplate.query(readSql, (rs, rowNum) -> rs.getString("id"), request.getId()).isEmpty();
        if (isExist) {
            throw new RuntimeException("존재 하지 않는 과일입니다.");
        }
        String updateSql = "UPDATE fruits SET isSold = true where id = ?";
        jdbcTemplate.update(updateSql, request.getId());
    }
}

 

  • 위 코드는 PUT 메서드로 데이터 수정을 위해 사용하는 HTTP 메서드이다.
  • @RequestBody 어노테이션으로 HTTP Body에서 과일 데이터를 받아온다.
  • 해당 과일 id를 가진 과일이 없다면 "존재하지 않는 과일"이라고 보여준다. (예외를 발생시킨다.)
  • 만약 있다면 해당 id를 가진 과일을 isSold 컬럼을 true로 바꿔준다.

 

2. DTO

public class FruitUpdateRequest {
    private long id;
    private String name;

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

 

3. 실행 결과

 

[실행 전 데이터]

 

[실행 후 데이터]

  • 1, 3번 데이터를 실행해보겠다. (3번은 생략)

요청 결과 200 반환

 

 

위와 같이 1번, 3번 데이터의 isSold 컬럼이 1로 바뀐 것을 볼 수 있다.

(mysql에서는 1은 true, 0은 false이다. 일반적으로 프로그래밍에서도 동일하다.)

 

출처 -&nbsp;https://inf.run/XKQg

 

1. Controller 코드

@RestController
public class FruitController {
    private final JdbcTemplate jdbcTemplate;

    public FruitController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @GetMapping("/api/v1/fruit/stat")
    public FruitResponse selectFruits(@RequestParam String name) {
        String sale = "SELECT SUM(price) as salesAmount FROM fruits 
        				WHERE isSold = true AND name = ?";
        String notSale = "SELECT SUM(price) as notSalesAmount FROM fruits 
        				WHERE isSold = false AND name = ?";

        long salesAmount = jdbcTemplate.queryForObject(sale, (rs, rowNum) 
        					-> rs.getLong("salesAmount"), name);
        long notSalesAmount = jdbcTemplate.queryForObject(notSale, (rs, rowNum) 
        					-> rs.getLong("notSalesAmount"), name);

        return new FruitResponse(salesAmount, notSalesAmount);
    }
}

 

  • HTTP 메서드는 GET 메서드로 name(과일 이름)을 매개변수로 받고 있다.
  • sale은 isSold가 true이면서 매개 변수로 받은 과일 이름과 일치하는 과일의 가격을 더한다.
  • notSale은 isSold가 false이면서 매개 변수로 받은 과일과 일치하는 과일의 가격을 더한다.
  • 그렇게 해서 출력 방식인 salesAmount와 notSaleAmount를 리턴을 한다.

2. 실행 결과

[실행 전 데이터]

 

[실행 후 데이터]

 

 

 

[질문] - SUM과 GROUP BY 키워드

SUM: 주어진 열(컬럼)의 값들을 모두 더하는 SQL 집계 함수이다. 위와 같이 가격을 합산해주는 용도로 사용한다.
GROUP BY: 집계 함수와 함께 사용되고, 특정 열(컬럼)의 값에 따라 결과를 그룹화합한다.

 

 

 

강의 링크 👉 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

728x90
728x90

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

람다는 함수형 프로그래밍이다.  함수형 프로그래밍은 프로그램을 작성하는 데 있어서 함수의 조합을 중심으로 하는 프로그래밍이다. 이는 병렬 처리 및 동시성 프로그래밍을 강조하고, 불변성과 순수 함수를 중시하여 안정적이고 예측 가능한 코드를 작성한다.

 

자바는 과거부터 객체지향 프로그래밍 언어로서의 강점을 가지고 있었지만, 함수형 프로그래밍의 이점도 놓치지 않기 위해 Java 8에서 람다식이 도입되었다. 람다는 코드의 간결성과 가독성을 높이고 병렬성을 활용할 수 있다. 또, 함수형 프로그래밍은 요즘 개발에 있어 중요한 요소로 자리 잡았다. 자바도 객체지향 프로그래밍을 보완하면서, 시대의 변화에 맞게 변화하기 위해 도입하였다.

 

 

람다식과 익명 클래스는 어떤 관계가 있을까?

 

1. 익명 클래스란?

Java에서 클래스는 사용자가 정의한 타입을 만드는

설계도로 객체를 생성하고 코드의 재사용성을 높이는 역할을 한다.

 

하지만 익명 클래스는 내부 클래스의 일종으로 말 그대로 이름 없는 클래스를 말한다.

익명 클래스는 단발성으로 한번만 사용해야 하는 객체일 경우 사용한다.

즉, 클래스와 같이 재사용성이 없고, 오히려 확장하여 사용하는 것이 불리할 때 사용한다.

 

그렇기 때문에 생성자를 선언할 수 없고, 단 하나의 클래스, 인터페이스를 상속 받거나 구현할 수 있다.

public class Anonymous {
    public static void main(String[] args){
        className variableName = new className(){ // 클래스이름 참조변수이름 = new 클래스이름
            // 내용 작성
        }
    }
}

 

2. 함수형 프로그래밍이란?

순수 함수를 조합하고 공유 상태를 변경 가능한 데이터 및 부작용을 피해 소프트웨어를 만든는 프로그래밍 방법이다.
선언형 프로그래밍으로, 애플리케이션의 상태는 순수 함수를 통해 전달된다.

(명령형 프로그래밍은 어떻게 할지 표현하고, 선언형 프로그래밍은 무엇을 할 것인지 표현한다.)

 

  • 순수 함수 (Pure Function)
    • 순수 함수는 동일한 입력 값에 대해 항상 같은 값을 반환해준다.
    • 전역 변수를 사용하거나, 변경하면서 생기는 부작용이 없다.

 

  • 1급 객체 (Frist Class Citizen)
    • 변수나 데이터 구조안에 담을 수 있다.
    • 파라미터로 전달 할 수 있다.
    • 반환값으로 사용할 수 있다.
    • 할당에 사용된 이름과 무관하게 고유한 구별이 가능하다. 
    • class가 없어도 독립적으로 메서드의 인자로 전달되고나 return 값으로 전달 받을 수 있다.
      즉, 함수를 데이터 다루듯이 사용할 수 있다.
  • 고차 함수(High Order Function)
    • 함수를 매개변수로 사용하거나 함수를 반환하는 함수.
  • 불변성(Immutable)
    • 데이터는 변하지 않는 불변성을 가지고 있다.
    • 데이터의 변경이 필요할 경우, 원본 데이터는 변경하지 않고 복사본을 만들어 변경하고 사용한다.

 

3. 함수형 인터페이스(FunctionalInterface)

함수형 인터페이스는 오직 한 개의 추상 메서드를 갖는 인터페이스이다.

@FunctionalInterface
interface FuncInterface {
    // 추상메서드
    int sum(int a, int b);
    
    // default 메서드
    default int minus(int a, int b);
    
    // static 메서드
    static int mul(int a, int b);
}

 

위 코드와 같이 @FunctionalInterface 어노테이션이 붙고, 오로지 추상 메서드는 한 개 있거나,

default가 붙거나 static이 붙은 메서드가 있는 것을 말한다.

이때 default 메서드와 static이 붙은 메서드는 함수형 인터페이스에 아무런 영향을 미치지 않는다.

 

Java에서는 기본적으로 많이 사용되는 함수형 인터페이스를 제공한다.

java.util.fuction 패키지는 일반적으로 자주 쓰이는 함수형 이터페이스를 미리 정의한 패키지이다.

그러므로 함수형 인터페이스를 만들기 보다는 이 패키지를 활용하는 것이 좋다.

출처 -&nbsp;https://joomn11.tistory.com/22

 

 

4. 람다란?

람다 함수는 함수형 프로그래밍 언어에서 사용되는 개념으로 익명 함수라고도 한다.
Java 8 부터 지원하고, 불필요한 코드를 줄여 가독성을 향상 시킨다.

 

  • 람다의 특징
    • 익명 함수로 이름을 가질 필요가 없다. 
    • 메소드의 매개변수로 전달되거나, 변수에 저장될 수 있다.
    • 익명 클래스는 생성할 때 많은 코드를 작성해야 하지만 람다는 불필요한 코드를 작성하지 않아도 된다.
    • 람다는 메서드처럼 특정 클래스에 종속되지 않는다. 하지만 메서드와 동일하게 사용할 수 있다.(파라미터, 리턴 등)
  • 람다의 장점
    • 코드를 간결하게 만들 수 있다.
    • 코드에 개발자의 의도가 명확히 드러나 가독성이 높다진다.
    • 함수를 만드는 과정 없이 한번에 처리 가능하여 생산성이 높아진다.
    • 병렬 프로그래밍에 용이하다.
  • 람다의 단점
    • 람다로 사용한 익명함수는 재사용이 불가능하다.
    • 재귀로 사용하기엔 부적절하다.
    • 디버깅이 어렵다.
    • 람다로만 사용하게 되면 중복 생성이 될 가능성이 있다.
  • 람다 사용 방법
interface Calculator{
	public void calc(int a, int b);
}

public class Main{
    public static void main(String[] args){
        Calculator sum = (a, b) -> {
            int answer = a + b;
            System.out.println("sum: " + answer);
        }
        sum.calc(10, 20);
    }
}

// 람다 방식
// (매개변수 선언) -> { 문장들 }

 

 

[정리]

익명 클래스로 작성한 코드보단 람다식으로 작성한 코드가 훨씬 가독성이 좋고 간결성 있다.

람다는 매개변수 타입을 추론할 수 있지만, 익명 클래스는 인터페이스의 타입을 선언해야한다.

interface Calculator{
	public void calc(int a, int b);
}


// 익명
public class Main {
    public static void main(String[] args) {
        Calculator sum = new Calculator() {
            @Override
            public void calc(int a, int b) {
                int answer = a + b;
                System.out.println("sum: " + answer);
            }
        };
        sum.calc(10, 20);
    }
}

// 람다
public class Main{
    public static void main(String[] args){
        Calculator sum = (a, b) -> {
            int answer = a + b;
            System.out.println("sum: " + answer);
        }
        sum.calc(10, 20);
    }
}

 

 

강의 링크 👉 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

728x90
728x90

1. 두 수를 입력하여, 다음과 같은 결과가 나오는 GET API 만들기

[결과 예시]

{
	"add": 덧셈 결과,
	"minus": 뺄셈 결과,
	"multiply": 곱셈 결과
 }

 

[작성 코드]

public class Inflearn{
    @GetMapping("/api/v1/calc")
    public Calculator calculator(
    @RequestParam("number1") int number1, 
    @RequestParam("number2") int number2) 
    {
        return new Calculator(number1, number2);
    }
}


public class Calculator {
    public int add;
    public int minus;
    public int multiply;

    public Calculator(int number1, int number2) {
        this.add = number1 + number2;
        this.minus = number1 - number2;
        this.multiply = number1 * number2;
    }
}

 

[코드 설명]

  • Get API로 만들어야 하기 때문에 @GetMapping을 사용하였다.
  • calculator 메서드는 number1, number2 두 개의 숫자를 파라미터로 받고 Caculator 객체를 리턴 해준다.
  • Caculator 클래스에서 생성자는 두 수를 받아 덧셈, 뺄셈, 곱셈을 하여 저장한다.
  • Caculator 클래스에서 작업이 끝나면 결과를 반환해준다.

 

[실행 결과]

 

2. 날짜를 입력하면, 무슨 요일인지 알려주는 GET API를 만들기

[결과 예시]

 {
	"dayOfTheWeek": "MON"
 }

 

[작성 코드]

public class Inflearn{
    @GetMapping("/api/v1/day-of-the-week")
    public DayOfTheWeek getDate(@RequestParam("date") String date) {
        return new DayOfTheWeek(date);
    }
}


import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.TextStyle;
import java.util.Locale;

public class DayOfTheWeek {
    public String dayOfTheWeek;

    public DayOfTheWeek(String date) {
        String[] dateArr = date.split("-");
        
        LocalDate localDate = LocalDate.of(Integer.parseInt(dateArr[0]), 
        Integer.parseInt(dateArr[1]), Integer.parseInt(dateArr[2]));
        
        DayOfWeek dayOfWeek = localDate.getDayOfWeek();
        
        String day = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
        
        this.dayOfTheWeek = day;
    }

}

 

[코드 설명]

  • GET API로 만들어야 하기 때문에 @GetMapping을 사용하였다.
  • getDate 메서드는 문자열 date를 파라미터로 받고 날짜 객체를 리턴 한다.
  • DayOfTheWeek 클래스에서 생성자는 문자열 형식의 date를 받아 처리를 한다.
    • 문자열 date를 "-" 기준으로 자른다.
    • LocalDate.of() 파라미터에는 년, 월, 일이 순서대로 들어가고 LocalDate 객체를 생성한다.
    • LocalDate 객체인 localDate에서 getDayOfWeek() 메서드를 사용하여 요일을 가져온다.
    • 가져온 요일을 getDisplayName()메서드를 사용하여 dayOfTheWeek 필드에 요일을 할당한다.
  • DayOfTheWeek 클래스에서 작업이 끝나면 dayOfTheWeek 필드에 할당된 값을 리턴한다.

[실행 결과]

 

💡 LocalDate.of()

더보기

LocalDate 클래스에서 제공해주는 LocalDate.of()를 살펴 보았다. 이 메서드는Year-Month-DayOfMonth순서로

파라미더가 들어가고,해당 날짜를 나타내는LocalDate객체를 생성하는 역할을 한다.

 

💡 getDayOfWeek()

더보기

getDayOfWeek()가  어떻게 동작하는지 궁금해서 확인해 보았다.

getDayOfWeek() 메서드는 enum을 반환하고, 이 enum은 요일을 나타내는데 사용한다.
그럼 위 코드에서 enum은 어떤 형식인지 궁금해서 또 들어가봤다.

of(int dayOfWeek) 메서드는 "MONDAY, TUESDAY"등 요일이 적힌 enum 클래스를 리턴 하고 있고,

이 메서드는 ISO-8601 표준에 따라 1부터 7까지의 값을 가지고, 1은 월요일을 나타내고, 7은 일요일을 나타낸다고 한다.

 

3. 여러 수를 받아 총 합을 반환하는 POST API 만들기

[결과 예시]

 {
	"numbers": [1, 2, 3, 4, 5]
 }

 

[작성 코드]

public class Inflearn{
    @PostMapping("/api/v1/sum")
    public int sum(@RequestBody NumberList numbers) {
        int sum = 0;
        for (Integer num : numbers.numbers) {
            sum += num;
        }

        return sum;
    }
}


import java.util.List;

public class NumberList {
    public List<Integer> numbers;
}

 

[코드 설명]

  • POST API로 만들어야 하기 때문에 @PostMapping을 사용하였다.
  • sum 메서드는 NumberList 객체인 numbers를 받는다.
  • NumberList 객체는 숫자 목록을 저장하기 위한 클래스이다.
    • numbers 필드는 숫자 목록을 저장하는 List<Integer> 타입이다.
  • NumberList 객체에 포함된 숫자 리스트를 반복하여 모든 숫자를 더한 값을 반환한다.

[실행 결과]

 

 

강의 링크 👉 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

728x90
728x90

1. 어노테이션이란?

먼저 어노테이션을 설명하기 전에 메타데이터에 대해 조금 알고 가야할 것 같다.

 

  • 메타데이터(Metatdata)란?
    • 데이터의 대한 속성 정보이다. '데이터에 관한 구조화 된 데이터', '다른 데이터를 설명해 주는 데이터'라고도한다.
    • 대량의 정보 가운데에서 찾고 있는 정보를 효율적으로 찾아내서 이용하기 위해 일정한 규칙에 따라 콘텐츠에 대하여 부여 되는 데이터라고 한다.
  • 어노테이션이란? 
    • 메타데이터의 일종이다.
    • 프로그램을 처리할 때 실행 과정에서 어떻게 데이터를 처리할지 알려주는 서브 데이터라고 한다.
    • 코드에서 @를 붙여 사용하는 코드
    • JDK 1.5버전 이상부터 사용가능

 

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

  • 컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공해준다.
  • 빌드나 배치시 코드를 자동으로 생성할 수 있도록 정보를 제공해준다.
  • 런타임 시 특정 기능을 실행하도록 정보를 제공해준다.

위와 같은 이유에서 사용한다. 그럼 어노테이션의 종류는 무엇이 있을까?

@Override

컴파일러에게 오버라이딩 하라는 메소드라고 알려준다.

 

@Service

스프링프레임 워크에서 자주 쓰이는 어노테이션으로 핵심 비즈니스 로직을 담은 서비스를 클래스 빈에 등록하기 위해 사용한다.

 

@SpringBootApplication

스프링 실행 시 필요한 다양한 설정을 알아서 자동으로 해준다. 

 

@GetMapping

스프링 프레임워크에서 자주 쓰이는 어노테이션으로 Http get요청이 오면 @GetMapping이 붙은 메소드가 실행된다.

 

위와 같이 많은 어노테이션이 있고 상황에 따라 알맞은 어노테이션을 사용하면 된다.

 

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

1. 어노테이션 생성

어노테이션은 인터페이스 앞에 @를 붙여 만들 수 있다.

public @interface MakeAnnotaion {
    
}

 

2. 메타 어노테이션

메타 어노테이션에는 아래와 같은 종류가 있다.

 

@Target : 사용할 어노테이션에 적용할 대상을 지정하는데 사용한다.

ElementType.ANNOTATION_TYPE 어노테이션
ElementType.CONSTRUCTOR 생성자
ElementType.FIELD 멤버변수, Enum 상수
ElementType.LOCALVARIABLE 지역변수
ElementType.METHOD 메서드
ElementType.PACKAGE  패키지
ElementType.PARAMETER 매개변수
ElementType.TYPE 타입(클래스, 인터페이스, Enum)
ElementType.TYPE_PARAMETER 타입 매개변수(제네릭과 같은 매개변수)
ElementType.TYPE_USE 타입이 사용되는 모든 대상

 

@Documented : 어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 하는 어노테이션이다.

자바에서 제공하는 표준 어노테이션과 메타 어노테이션 중에 @Override와 @SuppressWarnings 어노테이션을 제외하고 모든 어노테이션에 @Documented 어노테이션이 적용되어 있다.

 

@Inherited : 하위 클래스가 어노테이션을 상속받도록 한다.

상위 클래스에 @Inherited를 붙이면 하위 클래스에서도 동일하게 사용된다.

 

@Retention : 어노테이션의 지속 시간을 결정하는 데 사용한다.

RetentionPolicy.SOURCE 컴파일 전까지 지속됨, 컴파일 이후엔 사라진다.
RetentionPolicy.CLASS 런타임이 실행되기 전까지만 존재한다.
RetentionPolicy.RUNTIME
런타임 시 사용되된다. (즉, 클래스 파일까지 존재하며 실행시 사용된다.)

 

@Repeatable : 어노테이션을 반복하여 사용할 수 있도록 허용하는 어노테이션이다.

@Retention(RetentionPolicy.RUNTIME) // 어노테이션은 특정 어노테이션의 지속 시간을 결정하는 데 사용
@Inherited // 하위 클래스가 어노테이션을 상속받도록 함
@Documented // 어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함
@Target(ElementType.METHOD)// 사용할 어노테이션을 적용할 대상을 지정
public @interface MakeAnnotaion {
    
}

 

 

위의 코드는 컴파일 이후에도 사용할 수 있는 @Retention(RetentionPolicy.RUNTIME) 사용하였고, 하위 클래스에서 어노테이션을 상송 받을 수 있도록 하였다. 또, @Documented를 사용하여 해당 어노테이션에 대한 정보가 문서로 저장될 수 있도록 하였고, 메서드 선언 시 적용할 수 있도록 @Target(ElementType.METHOD)를 사용하였다.

위와 같이 필요한 어노테이션을 상황에 맞게 적용하여 만들면 된다.

 

출처 - https://ittrue.tistory.com/160

 

3. 만든 어노테이션 사용하기

@MakeAnnotation(name = "HelloController")
@RequestMapping("/hello")
public class HelloController {

}

public class HelloController2 {
	@MakeAnnotation
	String str = null;
    
    @MakeAnnotation
    public void test(){
    
    }
}

 

위와 같이 만든 어노테이션을 적용할 수 있다.

 

[느낀점]

어노테이션에 대해 궁금해 하지 않았는데 이번 기회로 어노테이션이 어떤건지 알 수 있게 되었다.

커스텀 어노테이션도 구현하는 것이 마냥 어렵다고 느꼈는데 각각 어떤 기능을 하는지 어디에 필요한지 정리하면 생각보다 쉽게 커스텀 어노테이션 구현이 가능하다는 것을 알게 되었다.

조금 더 공부해서 직접 어노테이션을 만들어 프로젝트에도 한번 적용해보아야겠다.😁

 

강의 링크 👉 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

728x90
728x90

어? 강의가 되게 재밌어 보이네?

오랜 기간 취업 준비를 하는 도중 프로젝트 없이 공부만 하다보니 개발이 재미 없어지기 시작하였습니다.

물론 오랜 취업 준비 기간으로 힘든 것도 있지만 내가 직접 만드는 서비스가 없다보니 흥미가 떨어진 것 같다고 생각하였습니다. 그렇게 어떤 프로젝트를 할까, 개인으로 할까? 팀으로 할까? 많은 고민을 하던 중 '자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지!' 라는 강의를 알게 되었습니다.

강의 목차를 보니 웹 개발에 필요한 기술부터 네트워크, AWS 등 정말 백엔드로서 필요한 지식을 한 강의에서 제공해주는 것에 매력을 느꼈습니다. 하지만 돈 없는 취준생은 듣고 싶지만 부담이 되었습니다.😭

 

이건 꼭 해야해..!

그렇게 며칠을 고민하던 중 지식 공유자께서 직접 참여를 하고, 다양한 사람들과 의견을 주고 받을 수 있는 스터디가 열린다는 것을 알게 되었습니다. 거기에 대상 강의가 바로..! 제가 구매할지 말아야할지 고민하던 '자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지!' 였습니다.

저는 개발 실력도 중요하지만 네트워킹에 참여하는 것도 중요하다고 생각하였는데 이 기회는 두마리의 토끼를 동시에 잡을 수 있는 매력적인 이벤트였습니다. 강의를 사야할지 말아야할지 고민하던 저는 주저 없이 스터디를 신청하게 되었고, 강의도 바로 구매하였습니다.😁

 

그렇게 시작한 OT(0일차)

16일 금요일 스터디의 시작을 알리는 OT를 하였습니다. 지식공유자이신 최태현님께서 대략적인 스터디 일정을 알려주셨고, 자바의 역사에 대해 알려주셨습니다.

스터디 일정을 보니 정말 타이트하고 힘들 것 같지만 저는 시간 많은 취준생 아니..백수이기 때문에 열심히 스터디 일정에 맞춰서 달려보겠습니다. 이번 기회로 참여하신 다른 러너분들과 많은 이야기와 정보를 주고 받고 싶습니다.

 

오랜 취업준비를 하였지만 그런 것 치고는 큰 실력향상이 있진 않았던 것 같아 이번 기회에 제대로 마음 잡고 공부해보려고 합니다..! 화이팅..! (급 종료)

 

 

강의 링크 👉 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]

728x90

+ Recent posts