이번 3주차에서는 JPA 연관관계 매핑하기 강의에서 만들었던 도서관리 프로그램 배포하기를 하였다.
JPA를 조금 더 객체지향적으로 코드를 고쳐볼 수 있었다.
[3주차 과제]
3주차에서 진행한 미니프로젝트는 출퇴근 사내 관리프로그램이었다.
JPA에 대한 개념이 많이 부족해 생각한 대로 프로그램은 돌아가는데 제대로 코드를 작성한게 맞는지, 코드는 깔끔하게 짰는지 등등 많은 생각이 들었다. 그래도 이번 강의를 통해서 JPA가 무엇인지 어떻게 사용하는지 그리고 스프링에 대한 개념에 대해 조금 더 와닿게 되었다.
오래된 취준 기간으로 약간의 강제성이 필요할 때 우연히 인프런 워밍업 클럽 스터디를 접하게 되었고, 현재 나에게 제일 필요했던 공부인 JPA를 공부할 수 있어 정말 뜻 깊은 스터디였다. 단순 강의만 듣는 것이 아닌 미션과 미니프로젝트를 병행하여서 강의 내용을 보다 더 쉽게 이해할 수 있었다.
이번 스터디로 습득한 지식을 바탕으로 조금 더 공부하여 나머지 2,3,4단계까지 마무리 해보고 개인 서버에 배포까지 해볼 예정이다.
@ServicepublicclassFruitServiceV2{
privatefinal FruitRepository fruitRepository;
publicFruitServiceV2(FruitRepository fruitRepository){
this.fruitRepository = fruitRepository;
}
public FruitResponse selectFruits(String name){
long salesAmount = fruitRepository.selectSalesAmount(name);
long notSalesAmount = fruitRepository.selectNotSalesAmount(name);
returnnew FruitResponse(salesAmount, notSalesAmount);
}
publicvoidinsertFruits(FruitRequest request){
fruitRepository.save(new Fruits(request.getName(),
request.getWarehousingDate(), request.getPrice()));
}
publicvoidupdateFruits(FruitUpdateRequest request){
Fruits fruit = fruitRepository.findById(request.getId())
.orElseThrow(() -> new IllegalArgumentException("해당하는 과일이 없습니다."));
fruit.updateFruit();
fruitRepository.save(fruit);
}
}
Repository
publicinterfaceFruitRepositoryextendsJpaRepository<Fruits, Long> {
@Query(value = "SELECT SUM(price) as salesAmount FROM fruits
WHERE is_sold = true AND name = :name", nativeQuery = true)longselectSalesAmount(@Param("name") String name);
@Query(value = "SELECT SUM(price) as notSalesAmount FROM fruits
WHERE is_sold = false AND name = :name", nativeQuery = true)longselectNotSalesAmount(@Param("name") String name);
}
Sql에서 SUM() 함수를 사용하여야 하는데 어떻게 해야할지 고민하다가 검색해보니 @Query어노테이션으로 사용자 정의 쿼리를 사용할 수 있는 것을 알게 되었다. @Query는 실행할 메서드 위에 정적 쿼리를 작성하는 방식으로 사용된다.
강사님 코드를 보며 하나하나씩 바꿔 보았지만 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까지 주석 처리를 한 뒤 오류는 해결 되었고 프로그램이 정상적으로 실행 될 수 있었다.
@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);
}
}
public class FruitCountResponse {
private long count;
public FruitCountResponse(long count) {
this.count = count;
}
public long getCount() {
return count;
}
}
@RepositorypublicclassFruitRepository{
private final JdbcTemplate jdbcTemplate;
publicFruitRepository(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);
returnnew FruitResponse(salesAmount, notSalesAmount);
}
publicvoidinsertFruits(String name, LocalDate warehousingDate, long price) {
String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, name, warehousingDate, price);
}
publicbooleanisExist(long id) {
String readSql = "SELECT * FROM fruits WHERE ID = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
publicvoidupdateFruits(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해주었다.
@RepositorypublicclassFruitMemoryRepositoryimplementsFruitRepository{
privatefinal JdbcTemplate jdbcTemplate;
publicFruitMemoryRepository(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
@Overridepublic 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);
returnnew FruitResponse(salesAmount, notSalesAmount);
}
@OverridepublicvoidinsertFruits(String name, LocalDate warehousingDate, long price){
String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, name, warehousingDate, price);
}
@OverridepublicbooleanisExist(long id){
String readSql = "SELECT * FROM fruits WHERE id = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
@OverridepublicvoidupdateFruits(long id){
String updateSql = "UPDATE fruits SET isSold = true WHERE id = ?";
jdbcTemplate.update(updateSql, id);
}
}
FruitMysqlRepository
@Repository@PrimarypublicclassFruitMySqlRepositoryimplementsFruitRepository{
privatefinal JdbcTemplate jdbcTemplate;
publicFruitMySqlRepository(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
@Overridepublic 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);
returnnew FruitResponse(salesAmount, notSalesAmount);
}
@OverridepublicvoidinsertFruits(String name, LocalDate warehousingDate, long price){
String sql = "INSERT INTO fruits (name, warehousingDate, price) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, name, warehousingDate, price);
}
@OverridepublicbooleanisExist(long id){
String readSql = "SELECT * FROM fruits WHERE id = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
@OverridepublicvoidupdateFruits(long id){
String updateSql = "UPDATE fruits SET isSold = true WHERE id = ?";
jdbcTemplate.update(updateSql, id);
}
}
이렇게 위의 요구사항과 같이 Interface로 나누고 FruitMemoryRepository와 FruitMySqlRepository가 FruitRepository를 구현하도록 하였다. 이렇게 되면 Service에 코드 수정 없이 Repository를 Memory로 사용할지 Mysql로 사용할지 사용할 수 있다.
💡그렇다면 어떻게 사용할까?
현재 FruitMySqlRepository를 보면 @Primary 어노테이션이 있다. 이 어노테이션은 "FruitMySqlRepository을 사용할거야~"라고 알려주는 어노테이션이다. 만약 @Primary가 없다면 Service 코드의 생성자 부분에서 오류가 나게된다.
다음으론 @Qualifier어노테이션이 있다. @Qualifier("fruitMemoryRepository")를 Service 코드의 생성자 부분에 사용하면 FruitMySqlRepository에 @Primary가 붙어 있더라도 fruitMemoryRepository를 사용하게 된다.
@Qualifier 어노테이션은 @Qualifier("fruitMemoryRepository") 이런 방식이 아니어도 아래와 같은 방식으로도 사용할 수 있다.
// 서비스 코드@ServicepublicclassFruitService{
privatefinal FruitRepository fruitRepository;
publicFruitService(@Qualifier("fruit")FruitRepository fruitRepository){
this.fruitRepository = fruitRepository;
}
// 이하 생략
}
// 레포지토리 코드@Repository@Qualifier("fruit")publicclassFruitMemoryRepositoryimplementsFruitRepository{
// 이하 생략
}
사용하려는 곳에 직접 이름을 지정해주고, 지정해준 이름을 생성자 앞에 적어주는 방식으로도 사용할 수 있다.
@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 어노테이션을 추가하거나 IntelliJ 의 Settings메뉴에서 Editor > Inspections로 이동하여 "Incorrect autowiring in Spring bean components" 검색하여 해당 인스펙션의 옵션을 수정하라고 하였다.
자세히 강의를 보니 강사님께서 클래스 이름을 FruitMemoryRepository로 작성하셨는데 첫 글자를 소문자로 작성하신것을 보고 바꾸니까 에러가 없어졌다.
왜 그런지 아직 잘 모르겠다.. 조금 찾아보아야겠다.
[이유를 알아냈다..!]
@Qualifier는 직접 빈으로 등록해준 것이 아니라면 빈으로 설정된 Default 이름을 적어주어야한다.
가상화: 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에 속한다.
아무런 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