728x90

[오프라인 수료식 후기]

3주간의 시간이 지나고, 워밍업 클럽 스터디가 끝나면서 오프라인 수료식을 다녀왔다.

 

[첫번째 순서]

낯을 조금 가리는 탓에 처음 도착해서는 앉아서 멀뚱멀뚱 사무실만 구경하고 있었다.

감사하게도 같은 테이블에 계신 러너분께서 먼저 대화를 걸어주셔서 수료식 시작 전까지 신나게 대화하였고,

다른 러너 두분께서 추가로 오셔서 첫번째 순서인 저녁 식사를 하며 또 다시 수다 타임을 가졌다.

 

[두번째 순서]

식사를 마치고 두번째 순서는 지식 공유자분들의 Q&A 타임이었다.

질문은 사전에 러너분들께서 작성해주신 질문에서 몇개 골라 선정된 질문들이었다.

취업에 관한 질문, 개발자에 대한 질문 등등 여러가지 질문들이 있었는데, 그동안 내가 많이 고민했던 질문들이 많았고

내가 고민했던 것과 지식공유자분들이 답변해주신 것과 일치해서 조금은 안도(?) 하였다.🤣

 

[세번째 순서]

이후 세번째 순서는 우수러너 시상식이었다. 나는 처음부터 우수러너에 대한 기대는 크게 없었다.

블로그도 누군가에게 보여주기 보다는 내가 공부하고 기록하는 곳으로 사용했고 깔끔하게 작성하는 편도 아니다.

열심히 해보겠다는 의지는 강했지만 실력자분들이 많아 이런 쉬운 질문을 해도 될까? 라는 생각에 질문을 망설였다.

(조금 더 찾아보니 혼자서도 충분히 해결 가능했던 질문들이 많았다ㅎㅎ)

위와 같은 이유로 우수러너는 포기하였고, 완주와 내 실력 향상에 초점을 맞추어 묵묵히 열심히 하였다.

 

그런데!!

우수러너 명단에 내 이름이 있던 것이었다!!

감사하게도 우수러너로 선정되어 인프런의 귀여운 굿즈를 받게 되었다!

다시 한번 감사드립니다.

 

[마지막 순서]

그렇게 기쁜마음으로 시상식을 마치고 마지막 순서인 네트워킹을 가졌다.

조별로 나누어 앉았고 각 조별로 코치님과 인프런 개발자 한분씩 들어가 네트워킹을 가졌다.

내가 있던 테이블에는 인프런에서 5년째 일하고 계신 프론트엔드 개발자분이셨고,

현재 취업 시장이 어떤지, 현업에서는 어떻게 일하는지 등등 유익한 이야기를 나누며 수료식은 끝이 났다.

 

[스터디를 하면서 느낀점]

그동안 공부하면서 이해되지 않았던 부분이 이번 강의를 통해 이해가 되어 고구마 100개 먹은 가슴이 뻥 뚫렸고,

궁금한 부분에 대해 해결하고 고민하는 방법을 얻을 수 있어서 좋았다.

 

이전엔 오류가 나고 해당 매서드에 대해 궁금하면 무작정 인터넷을 먼저 찾았지만

이제는 해당 매서드에 들어가서 어떤 것들로 구성되어 있고, 해당 오류는 어떤 오류인지 공식문서나 해당 코드를 통해

먼저 생각하고 고민해보는 습관이 형성되어 좋았다.

 

짧다면 짧고 길다면 긴 3주동안 재미있게 공부하며 즐겼던 스터디였다.

 

스터디를 진행해주시고 늦은 시간까지 수료식을 위해 자리를 만들어주신 인프랩 임직원분들,

바쁘실텐데 중간 중간 깜짝 특강을 진행해주시고 예정된 시간이 지나도 목소리가 쉬어가며 질문과 정보 전달을 위해

열정을 보여주신 최태현 코치님께 감사인사 드립니다.

그리고 3주간 함께 달리신 러너분들 고생 많으셨습니다!😁

728x90
728x90

[3주차 학습내용]

이번 3주차에서는 JPA 연관관계 매핑하기 강의에서 만들었던 도서관리 프로그램 배포하기를 하였다.

JPA를 조금 더 객체지향적으로 코드를 고쳐볼 수 있었다.

 

[3주차 과제]

3주차에서 진행한 미니프로젝트는 출퇴근 사내 관리프로그램이었다.

JPA에 대한 개념이 많이 부족해 생각한 대로 프로그램은 돌아가는데 제대로 코드를 작성한게 맞는지, 코드는 깔끔하게 짰는지 등등 많은 생각이 들었다. 그래도 이번 강의를 통해서 JPA가 무엇인지 어떻게 사용하는지 그리고 스프링에 대한 개념에 대해 조금 더 와닿게 되었다.

미니프로젝트 👉 https://ddonydev.tistory.com/83

 

[마무리 및 느낀점]

오래된 취준 기간으로 약간의 강제성이 필요할 때 우연히 인프런 워밍업 클럽 스터디를 접하게 되었고, 현재 나에게 제일 필요했던 공부인 JPA를 공부할 수 있어 정말 뜻 깊은 스터디였다. 단순 강의만 듣는 것이 아닌 미션과 미니프로젝트를 병행하여서 강의 내용을 보다 더 쉽게 이해할 수 있었다.

이번 스터디로 습득한 지식을 바탕으로 조금 더 공부하여 나머지 2,3,4단계까지 마무리 해보고 개인 서버에 배포까지 해볼 예정이다.

 

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

728x90
728x90

 

1. 팀

Table

 

Entity

@Entity
@Getter
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @Column(nullable = false, length = 20)
    private String name;
    @Column(nullable = false, length = 20)
    private String manager;
    @Column(nullable = false)
    private long employeeCnt;

    protected Team(){}

    public Team(String name, String manager, long employeeCnt) {
        this.name = name;
        this.manager = manager;
        this.employeeCnt = employeeCnt;
    }

    public void updateEmployeeCnt() {
        this.employeeCnt++;
    }

    public void updateManager(String name) {
        this.manager = name;
    }
}

 

1

Controller

@RestController
@RequestMapping("/attendance/team")
public class TeamController {
    private final TeamService service;

    public TeamController(TeamService service) {
        this.service = service;
    }

    @PostMapping("/resister_team")
    public void resisterTeam(@RequestBody RegisterTeamRequest request) {
        service.insertTeam(request);
    }
}

 

Service

@Service
public class TeamService {

    private final TeamRepository repository;

    public TeamService(TeamRepository repository) {
        this.repository = repository;
    }

    public void insertTeam(RegisterTeamRequest request) {
        repository.findByName(request.getName()).ifPresent(team -> {
            throw new IllegalArgumentException("이미 존재하는 팀 이름입니다.");
        });

        repository.save(new Team(request.getName(), 
        request.getManager(), request.getMemberCnt()));
    }
}

 

  • RegisterTeamRequest 객체에서 팀 이름을 꺼내와 등록된 팀이 있는지 확인해준다.
  • 팀이 등록 되어 있다면 Exception을 발생시키고 없다면 새로운 팀을 만들어준다.
  • 팀 등록은 이름, 매니저, 그리고 팀 인원수를 가진 Team 객체를 생성하여 저장된다.

 

Repository

public interface TeamRepository extends JpaRepository<Team, Long> {
     Optional<Team> findByName(String name);
}

 

 

DTO

@Getter
public class RegisterTeamRequest {
    private long id;
    private String name;
    private String manager;
    private long memberCnt;

}

 

 

 

Controller

@RestController
@RequestMapping("/attendance/team")
public class TeamController {
    private final TeamService service;

    public TeamController(TeamService service) {
        this.service = service;
    }

    @GetMapping("/team")
    public List<TeamListResponse> teamList() {
        return service.selectTeamList();
    }
}

 

Service

@Service
public class TeamService {

    private final TeamRepository repository;

    public TeamService(TeamRepository repository) {
        this.repository = repository;
    }

    public List<TeamListResponse> selectTeamList() {
        return repository.findAll().stream()
                .map(team -> new TeamListResponse(team.getName(), 
                team.getManager(), team.getEmployeeCnt()))
                .collect(Collectors.toList());
    }
}
  • JPA에 findAll 메서드를 사용하여 테이블에 저장된 모든 데이터를 가져온다.
  • map()메서드는 TeamListResponse 객체로 변환해준다.
  • collect()메서드를 사용하여 원하는 데이터 타입으로 변환하여 반환해준다.

DTO

@Getter
public class TeamListResponse {
    private String name;
    private String manager;
    private long memberCnt;

    public TeamListResponse(String name, String manager, long memberCnt) {
        this.name = name;
        this.manager = manager;
        this.memberCnt = memberCnt;
    }
}

 

2. 직원

Table

 

Entity

@Entity
@Getter
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @Column(nullable = false, length = 20)
    private String name;
    @Column(nullable = false, length = 20)
    private String teamName;
    @Column(nullable = false, length = 10)
    @Enumerated(EnumType.STRING)
    private Role role;
    @Column(nullable = false)
    private LocalDate birthday;
    @Column(nullable = false)
    private LocalDate workStartDate;

    protected Employee() {}

    public Employee(String name, String teamName, Role role, 
    				LocalDate birthday, LocalDate workStartDate) {
        this.name = name;
        this.teamName = teamName;
        this.role = role;
        this.birthday = birthday;
        this.workStartDate = workStartDate;
    }
}

 

 

Controller

@RestController
@RequestMapping("/attendance/employee")
public class EmployeeController {

    private final EmployeeService service;

    public EmployeeController(EmployeeService service) {
        this.service = service;
    }

    @PostMapping("/register_employee")
    public void resisterEmployee(@RequestBody ResisterEmployeeRequest request) {
        service.insertEmployee(request);
    }
}

 

Service

@Service
public class EmployeeService {

    private final EmployeeRepository repository;
    private final TeamService teamService;

    public EmployeeService(EmployeeRepository repository, TeamService teamService) {
        this.repository = repository;
        this.teamService = teamService;
    }

    @Transactional
    public void insertEmployee(ResisterEmployeeRequest request) {
    	if (request.getRole() == null) {
            throw new IllegalArgumentException("직급을 입력해주세요.");
        }
        
        repository.findById(request.getId()).ifPresent(employee -> {
            throw new IllegalArgumentException("이미 등록된 직원입니다.");
        });

        repository.save(new Employee(request.getName(), request.getTeamName(),
                request.getRole(), request.getBirthday(), request.getWorkStartDate()));

        if (Role.MANAGER.equals(request.getRole())) {
            teamService.updateManager(request.getTeamName(), request.getName());
        }

        teamService.updateEmployeeCnt(request.getTeamName());
    }
}
  • Role이 null인지 확인하고 없다면 예외를 발생시킨다.
  • Id를 사용하여 이미 등록되어 있는 직원인지 확인한다.
  • 있다면 예외를 발생시키고 없다면 새로운 직원을 만들어준다.
  • Employee 객체를 만들어 새로운 직원을 저장한다.
  • 이때 Role이 매니저인 경우 teamService의 updateManager메서드를 실행시킨다.
  • teamService의 updateEmployeeCnt 메서드를 실행하여 해당 팀의 직원 수를 증가시킨다.

TeamService

@Service
public class TeamService {

    private final TeamRepository repository;

    public TeamService(TeamRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public void updateEmployeeCnt(String teamName) {
        Team team = findTeamByName(teamName);

        team.updateEmployeeCnt();
    }

    @Transactional
    public void updateManager(String teamName, String name) {
        Team team = findTeamByName(teamName);

        team.updateManager(name);
    }

    private Team findTeamByName(String name) {
        return repository.findByName(name)
        	.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 팀 입니다."));
    }
}

 

  • findTeamByName 메서드를 사용하여 존재하는 팀 인지 확인한다.
  • 있다면 각 메서드를 실행하여 DB에 저장해준다.

Repository

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Optional<Employee> findById(long id);
}

 

DTO

@Getter
public class ResisterEmployeeRequest {
    private long id;
    private String name;
    private String teamName;
    private Role role;
    private LocalDate birthday;
    private LocalDate workStartDate;
}

 

Controller

@RestController
@RequestMapping("/attendance/employee")
public class EmployeeController {

    private final EmployeeService service;

    public EmployeeController(EmployeeService service) {
        this.service = service;
    }

    @GetMapping("/employee_list")
    public List<EmployeeListResponse> employeeList() {
        return service.selectEmployeeList();
    }
}

 

Service

@Service
public class EmployeeService {

    private final EmployeeRepository repository;
    private final TeamService teamService;

    public EmployeeService(EmployeeRepository repository, TeamService teamService) {
        this.repository = repository;
        this.teamService = teamService;
    }

    public List<EmployeeListResponse> selectEmployeeList() {
        return repository.findAll().stream()
                .map(employee -> new EmployeeListResponse(employee.getName(), 
                	employee.getTeamName(),employee.getRole(), employee.getBirthday(), 
                    employee.getWorkStartDate()))
                .collect(Collectors.toList());
    }
}
  • JPA에 findAll 메서드를 사용하여 테이블에 저장된 모든 데이터를 가져온다.
  • map()메서드는 TeamListResponse 객체로 변환해준다.
  • collect()메서드를 사용하여 원하는 데이터 타입으로 변환하여 반환해준다.

DTO

@Getter
public class EmployeeListResponse {
    private String name;
    private String teamName;
    private Role role;
    private LocalDate birthday;
    private LocalDate workStartDate;

    public EmployeeListResponse(String name, String teamName, Role role, 
    		LocalDate birthday, LocalDate workStartDate) {
        this.name = name;
        this.teamName = teamName;
        this.role = role;
        this.birthday = birthday;
        this.workStartDate = workStartDate;
    }
}

 

github - https://github.com/ddonydev/inflearn_study

 

[정리]

JPA를 다뤄본지 얼마 되지 않아 조금 어려웠고,

계층을 어떻게 나누어야할지 모르겠어서 그냥 강사님이 강의해서 해주신 그대로 만들어 보았다.

 

직원을 등록할때 Role이 Manager인 경우 팀 테이블에 있는 Manager 컬럼에 해당 직원의 이름을 넣어주고,

등록할때 마다 직원의 수를 update해주고 싶었는데 이를 어떻게 해결해야할지 이틀을 고민하였다.

 

그래서 생각한 것이 직원을 구현하고 있는 Service에 팀 Service를 주입 받아 사용하는 방법 밖에 떠오르지 않았고,

이게 맞는 것인지 다시 고민에 빠졌다.

 

"Service에 다른 도메인 Service 호출"이라는 키워드로 검색해 보았고, 의견은 많이 갈리는 것 같았다.

Controller에 여러 서비스를 호출하여 사용하는 방법, Service는 무조건 DAO와의 의존 관계를 갖게 하는 등 여러가지 방법이 있었다.

 

하지만 나는 JPA로 사용했기 때문에 DAO에 해당하는 Mapper는 사용하지 못할 것이라고 판단하였고,

Controller에서 여러 서비스를 호출하면 트랜잭션 문제가 생긴다는 글을 보았다.

 

그래서 결국 내가 처음 생각한 Service에 팀 Service를 주입 받아 사용하는 방법을 사용하여

내가 원하던 기능을 구현할 수 있었다.

 

이게 맞는 것인지 잘 모르겠고 아직 트랜잭션에 대한 개념이 확실하지 않아 조금 더 공부한 뒤

만들어본 코드를 다듬어 보아야겠다.

 

잘못된 부분 부족한 부분이 있다면 편하게 댓글로 남겨주세요☺️

 

 

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

728x90
728x90

[2주차 학습 내용]

 

벌써 스터디를 시작한지 2주차가 되어간다.

2주차에는 컨테이너가 무엇인지, 어떻게 동작하는지 JPA 사용해보기, 트랜잭션

그리고 조금 더 객체지향 적으로 코드 리팩토링 하는 방법을 배워 보았다.

 

[2주차 과제]

이번 2주차에는 기존에 Controller에서 그대로 SQL을 작성했던 것을 Controller - Service - Repository로 분리해보았고,

Repository에 SQL을 사용하는 것이 아닌 JPA를 사용하여 DB와 통신할 수 있도록 해보았다.

6일차 과제 보기 - https://ddonydev.tistory.com/80

7일차 과제 보기 - https://ddonydev.tistory.com/81

 

[느낀점]

다른 스프링 강의를 들었을 땐 완벽하게 이해가 되지 않고 넘어 갔었고, 정확하게 어떤 일을 하는 지 어떻게 동작하는지 와닿지 않았었다.

하지만 이번 강의를 들으면서 정말 완벽하진 않더라도 어느정도 이해가 되었고, 미션과 병행하며 코드를 쳐보니 조금 더 이해가 수월 했던 것 같다.

 

또, 그동안은 내가 A라고 알고 있던 것이 전혀 다르게 동작할때 또는 에러가 날때 등 그냥 넘어가거나 에러만 잡기 급급했는데

왜 에러가 났는지, 또 왜 전혀 다르게 동작했는지 이 메서드는 어떤 역할을 하는지 등등 어떻게 궁금해하고 어떻게 찾아가야하는지

생각하는 능력이 성장한 것 같아 너무 기뻤다.

 

이번에 처음 써본 JPA는 너무 어려웠지만 조금 더 공부해본 뒤 미니프로젝트를 진행해볼 예정이다.

이번 주는 웹 개발의 전반적인 흐름과 생각하는 능력을 성장시킬 수 있어서 뜻깊은 한주였다.

 

 

 

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

728x90
728x90

과제 6번 코드 보기 👉 https://ddonydev.tistory.com/80

 

 

  • Controller
@RestController
public class FruitController {

    private final FruitServiceV2 fruitService;

    public FruitController(FruitServiceV2 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 FruitServiceV2 {

    private final FruitRepository fruitRepository;

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

    public FruitResponse selectFruits(String name) {
        long salesAmount = fruitRepository.selectSalesAmount(name);
        long notSalesAmount = fruitRepository.selectNotSalesAmount(name);
        return new FruitResponse(salesAmount, notSalesAmount);
    }

    public void insertFruits(FruitRequest request){
        fruitRepository.save(new Fruits(request.getName(), 
        request.getWarehousingDate(), request.getPrice()));
    }

    public void updateFruits(FruitUpdateRequest request) {
        Fruits fruit = fruitRepository.findById(request.getId())
                .orElseThrow(() -> new IllegalArgumentException("해당하는 과일이 없습니다."));

        fruit.updateFruit();

        fruitRepository.save(fruit);
    }
}

 

  • Repository
public interface FruitRepository extends JpaRepository<Fruits, Long> {

    @Query(value = "SELECT SUM(price) as salesAmount FROM fruits 
    WHERE is_sold = true AND name = :name", nativeQuery = true)
    long selectSalesAmount(@Param("name") String name);

    @Query(value = "SELECT SUM(price) as notSalesAmount FROM fruits 
    WHERE is_sold = false AND name = :name", nativeQuery = true)
    long selectNotSalesAmount(@Param("name") String name);

}

 

 

Sql에서 SUM() 함수를 사용하여야 하는데 어떻게 해야할지 고민하다가 검색해보니 @Query어노테이션으로 사용자 정의 쿼리를 사용할 수 있는 것을 알게 되었다. @Query는 실행할 메서드 위에 정적 쿼리를 작성하는 방식으로 사용된다.

메서드에 @Param 어노테이션으로 조건에 사용할 변수를 지정해주어 사용할 수 있었다.

 

  • Fruit
@Entity
public class Fruits {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false, length = 20)
    private String name;

    @Column(nullable = false)
    private LocalDate warehousingDate;

    @Column(nullable = false)
    private long price;

    @Column(nullable = false, columnDefinition = "boolean default false")
    private boolean isSold = false;

    protected Fruits() {}

    public Fruits(String name, LocalDate warehousingDate, long price) {
        this.name = name;
        this.warehousingDate = warehousingDate;
        this.price = price;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }

    public long getPrice() {
        return price;
    }

    public boolean isSold() {
        return isSold;
    }

    public void updateFruit() {
        this.isSold = true;
    }


}

 

기존 코드를 JPA로 고치느라 엄청 애먹었다..😭

강사님 코드를 보며 하나하나씩 바꿔 보았지만 SQL의 SUM() 함수를 어떻게 사용해야할지 감이 오지 않았고, 어찌어찌 다 고치고 나니 Property가 없다는 에러가 났다.

No property 'updateFruits' found for type 'Fruit'

 

검색 해보니 클래스 명이 맞지 않거나 카멜케이스로 인한 오류 존재하지 않은 필드명을 사용한 쿼리 메서드를 만들었을 경우 오류가 난다는 것이었다.

그런데 아무리 봐도 내 코드엔 이상이 없었다. 그렇게 몇시간을 찾아보았는데..인터페이스에 내가 Service에 있는 메서드를 다 적어 놓은 것이었다..이걸 지우고 나니 아래와 같은 오류가 낫다.

Parameter 0 of constructor in com.group.libraryapp.controller.fruits.FruitService required a bean of type 'com.group.libraryapp.controller.fruits.FruitRepository' that could not be found.

 

FruitRepository를 찾을 수 없다는 것이었다. 찾아보니 이전에 만들었던 FruitService코드에 FruitRepository가 주입 된 상태였고 나는 FruitRepositoryV2를 사용하려고 했기 때문에 FruitRepository를 주석 처리하여 파일을 사용하지 않게 하니 당연히 못찾는 것이었다. 그래서 FruitServiceV2까지 주석 처리를 한 뒤 오류는 해결 되었고 프로그램이 정상적으로 실행 될 수 있었다.

 

 

 

  • Controller
@RestController
public class FruitController {

    private final FruitServiceV2 fruitService;

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

    @GetMapping("/api/v1/fruit/count")
    public FruitCountResponse countFruits(@RequestParam String name) {
        return fruitService.countFruits(name);
    }
}

 

  • Service
@Service
public class FruitServiceV2 {

    private final FruitRepository fruitRepository;

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

    public FruitCountResponse countFruits(String name) {
        long count = fruitRepository.countByName(name);
        return new FruitCountResponse(count);
    }
}

 

  • Repository
public interface FruitRepository extends JpaRepository<Fruits, Long> {

    long countByName(String name);

}

 

  • FruitCountResponse
public class FruitCountResponse {
    private long count;

    public FruitCountResponse(long count) {
        this.count = count;
    }

    public long getCount() {
        return count;
    }
}

 

[실행 결과]

 

  • 사과를 조회 했을 때 4개가 나오는 것을 볼 수 있다.

 

 

 

  • Controller
@RestController
public class FruitController {

    private final FruitServiceV2 fruitService;

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

    @GetMapping("/api/v1/fruit/list")
    public List<FruitListResponse> listFruits(@RequestParam String option, @RequestParam long price) {
        return fruitService.listFruits(option, price);
    }
}

 

  • Service
@Service
public class FruitServiceV2 {

    private final FruitRepository fruitRepository;

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

    public List<FruitListResponse> listFruits(String option, long price) {
        if (option.equals("GTE")) {
            return fruitRepository.findAllByPriceGreaterThanEqualAndIsSoldIsFalse(price)
                    .stream()
                    .map(fruit -> new FruitListResponse(fruit.getName(), 
                    fruit.getPrice(), fruit.getWarehousingDate()))
                    .collect(Collectors.toList());
        } else if (option.equals("LTE")) {
            return fruitRepository.findAllByPriceLessThanEqualAndIsSoldIsFalse(price)
                    .stream()
                    .map(fruit -> new FruitListResponse(fruit.getName(), 
                    fruit.getPrice(), fruit.getWarehousingDate()))
                    .collect(Collectors.toList());
        }
        return new ArrayList<>();
    }
}

 

여러 결과를 받아야 하기 때문에 List타입으로 나올 수 있도록 하였다.

여기서 findAllByPriceGreaterThanEqualAndIsSoldIsFalse를 사용하였는데 하나하나 살펴보자면

findAll: 모든 결과를 가져오고

price: price를 조건으로 가져오는데

GreaterThanEqual: 해당 조건 이상인 것

LessThanEqual: 해당 조건 이하인 것

AndIsSoldIsFalse: 그리고 IsSold 칼럼이 False인 것

이라고 해석할 수 있다.

 

즉, 입력한 가격이 이상이면서 IsSold가 false인 것을 조회하는 것이다.

SELECT * FROM FRUITS WHERE PRICE > 10000 AND IS_SOLD = FALSE;

 

  • Repository
public interface FruitRepository extends JpaRepository<Fruits, Long> {

    List<Fruits> findAllByPriceGreaterThanEqualAndIsSoldIsFalse(long price);

    List<Fruits> findAllByPriceLessThanEqualAndIsSoldIsFalse(long price);

}

 

  • FruitCountResponse
public class FruitListResponse {
    private String name;
    private long price;
    private LocalDate warehousingDate;

    public FruitListResponse(String name, long price, LocalDate warehousingDate) {
        this.name = name;
        this.price = price;
        this.warehousingDate = warehousingDate;
    }

    public String getName() {
        return name;
    }

    public long getPrice() {
        return price;
    }

    public LocalDate getWarehousingDate() {
        return warehousingDate;
    }
}

 

 

[실행결과]

 

 

[느낀점]

오늘 JPA는 처음 사용해보았다.

처음부터 JPA로 작성하는 것이 아닌 기존 코드를 JPA로 바꾸면서 해보는 것이라 JPA를 어떻게 사용하는 것인지 조금은 알게 되었다.

에러로 몇시간을 잡고 있었는진 모르겠지만 그래도 너무 재미있게 코딩한 것 같아 뿌듯하다..!

이제 JPA에 대해 조금 더 자세히 공부하고 프로젝트에도 적용해보아야겠다.

 

 

 

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

728x90
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

 

 

[1주차 회고]

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

 

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

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

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

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

 

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

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

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

 

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

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

 

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

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

 

다음 한 주도 화이팅💪

 

 

 

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

728x90
728x90

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

 

출처 -&nbsp;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

+ Recent posts