반응형

1. 팀 등록

 

팀을 등록할 api의 명세이다.

요구사항은 간단하지만 막상 구현하려면 이렇게 간단하게만 작성할수는없다.

비즈니스적으로 예외사항에대한 처리를 해줘야하고

파라미터로 들어오는값도 검증이 필요하다면 해주어야한다.

 

팀 등록기능 

1. 팀을 등록할때에는 팀 이름을 필수로 가져야한다.

 

예외처리

1. 동일한 이름의 팀등록은 제외한다.

 

Controller

@RestController
@RequestMapping("/team")
public class TeamController {

    private final TeamService service;

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

    @PostMapping
    public void registerTeam(@RequestBody Map<String, String> requestJson){
        service.registerTeam(requestJson.get("teamName"));
    }
}

Repository

public interface TeamRepository extends JpaRepository<Team,Long> {
    Team findByTeamName(String teamName);
    
    //팀 이름을 조회하여 있으면 true 없으면 false를 리턴
    Boolean existsByTeamName(String teamName);
}

Service

@Service
public class TeamService {

    private final TeamRepository repository;

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

    public void registerTeam(String teamName) {
    	//이미 있는 팀이름을 등록하는 경우 에러처리
   	//팀 이름을 키로 지정해도 같은 문제를 해결할수있을것같다.
        if(isExistTeam(teamName)){
            throw new IllegalArgumentException("이미 존재하는 팀 이름입니다: " + teamName);
        }
        repository.save(new Team(teamName));
    }
    
    private boolean isExistTeam(String name){
        return repository.existsByTeamName(name);
    }
}

 


 

 

디자인팀이 데이터베이스에 잘 저장되었다. 

같은 이름의 팀을 저장해보자.

 

 


 

동일한 이름의 팀을 저장하려고 시도하면 

에러가 발생한다.

 

 

 


 

이미 존재하는 팀의 이름을 저장하려고 하면 에러가 발생하는것을 볼수있다. 

api응답으로도 에러메시지를 받을수있는데 이경우에는 controller에서 ResponseEntity를 리턴해줘야하는것 같다.

간단한 저장이라서 응답없이 처리하도록 해주었기때문에

api응답은 에러의 발생원인이 서버에게있다고하는 500에러를 보내게된다. 

 

여기서 문제가 발생한다. 에러를 발생시킨부분은 서버가 맞지만 잘못된 요청으로 인해 에러가 발생한것으로 

다시 잘 요청해달라는 의미로 클라이언트 에러인 400번대 에러를 보내줘야한다. (클라이언트 너의 책임이다!)

 

여기서 스프링 Controller의 에러처리 기능인 @RestControllerAdvice를 사용해주었다. (처음 써봤다.)

 


@RestControllerAdvice , @ControllerAdvice

두 어노테이션 모두 스프링에서 Controller의 전역적인 에러처리를 위해서 사용된다. 

@ControllerAdvice

페이지가 있는 컨트롤러에서 이용되며

주로 에러페이지로 이동하는데 사용한다고 한다(아직 안써봄)

 

@RestController

에러메시지를 Json으로 내려줄수있어서 api를 개발할때 사용한다(적용).

RestControllerAdvice는 ControllerAdvice를 포함하고있다

 

@RestControllerAdvice
public class commuteServiceException {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> illegalArgumentExceptionHandle(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body("error : " + e.getMessage());
    }

}

 

이렇게 에러를 처리해줄 클래스를 만들고 @RestControllerAdvice어노테이션을 달아주면 

컨트롤러에서 IllegarArgumentException이 발생했을때  모두 여기서 처리해서 에러응답을 보내주게된다.

에러코드는 400으로 나온다. badRequest

 

이제 팀을 저장하는 기능이끝났다. 


2. 팀장 등록

추가적인 기능을 위해

팀 테이블에 팀장의 컬럼을 추가시켜서

이 팀의 팀장이 누구인지의 데이터를 가질수있게 처리해줬다.

팀을 저장할때는 팀의 이름만 저장하기때문에 팀장값은 null로 가지고있을것이다.  

이제 이 팀장을 지정해주는 기능을 만들어주어야한다.

 

요구사항

1. 직원의 정보를 보내서 팀장을 저장할수있게해준다.

 

예외처리. 

x.

 

따로 예외처리를 해주지는 않았지만 나중에 비즈니스 요구사항에 따라 다양한 예외처리가 추가될수있을것같다.

특정 직급이상만 팀장으로 등록이 가능하다거나.

특정 직원은 등록이 불가능하게 처리한다거나 할수있을듯하다. 

(현재는 딱히 필요없어보여서 생략)

//DTO
public class TeamSetDto {
    private String teamName;
    private Integer leaderCode; // 팀장의 이름이나 id 둘중의 하나만 가지면 될것같음
    //private String leaderName;
}

//jpa를 통해서 id 나 이름이 있다면 직원정보를 조회할수있기때문에 두개중에 하나만있다면 
//조회해서 다른정보를 불러서 사용이 가능하다.

//Controller
@PostMapping("/setLeader")
public void setTeamLeader(@RequestBody TeamSetDto request){
    service.registerTeamLeader(request);
}

//Service
@Transactional
    public void registerTeamLeader(TeamSetDto request) {
        Team team = repository.findByTeamName(request.getTeamName());
        team.setTeamLeader(request.getLeaderCode());
    }
    
//팀원의 이름이나 정보로조회해서 저장하는경우.
@Transactional
    public void registerTeamLeader(TeamSetDto request) {
        Team team = repository.findByTeamName(request.getTeamName());
        Employee employee = memberRepository.findByName(request.getName());
        team.setTeamLeader(employee.getId());
    }

 

각 계층에서 추가한부분만 옮겨봤다.

팀의 정보와 팀장으로저장할 직원의 정보를받아서 팀에 넣어주면

영속성컨텍스트에의해 조회한 team 인스턴스가 변경된걸 캐치해서

자동으로 업데이트 해주게된다. 

 

개인적인 생각

지금은 그냥 postman을 통해서 요청값을 세팅하기때문에 문자열정보인 String이 더 편하지만

추후에 개발시에 프론트를 구성한다면 직원정보를 조회하고

보이는쪽은이름 실제 보내지는 value쪽은 직원의 id값이 들어가서

api에 id로 요청하는게 더 수월해지기때문에 id가 필요한 api로 개발하는게 더 좋은것같다.

조회는 id로 해야 db의 index로 인해서 더 빠르다는 얘기도 들어본것같은...

 

 

 


3. 팀 조회기능

//DTO
@Getter
@AllArgsConstructor // Jpa매핑용 , 모든파라미터생성자
public class TeamResponse {
    private String name;
    private String manager;
    private Long count;
}

//Controller
@GetMapping
public List<TeamResponse> getTeam(){
    return service.getTeam();
}

//Service
public List<TeamResponse> getTeam(){
        return repository.getAllTeam();
    }
    
//Repository
// Jpql사용
@Query("SELECT new Company.demo.commute.dto.response.TeamResponse(a.teamName, b.name, COUNT(c.id)) " +
                  "FROM Team a " +
                  "LEFT JOIN Employee b ON a.teamLeader = b " +
                  "LEFT JOIN Employee c ON a.teamId = c.team.teamId " +
                  "GROUP BY a.teamName, b.name")
    List<TeamResponse> getAllTeam();

조회결과.

 

이번에는 Jpql로 데이터를 조회했다.

jpa로 응답객체를 내주는데에는 여러가지방법이있는데 간단한조회라면 

그냥 객체를 findby로 조회해서 서비스레이어에서 매핑해서 응답객체를 내주는것도 괜찮은 방법인것같다. 

여기서는 팀장의 이름을 알아내기위해 직원 테이블도 조인해줘야하고 팀의 총인원을 집계해줘야해서

단순조회로는 코드가 너무 길어질것같아서 쿼리로 처리해 주었다.

Jpql에서 join을 사용하려면 Entity객체에서도 관계설정이 되어있어야한다.

이로인해서 기존 Entity코드에도 변경이 생겼다.

 

Team (Entity)

@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long teamId;
    
    private String teamName;
    
    @OneToOne
    @JoinColumn(name = "team_leader_id")
    private Employee teamLeader;
    //private Integer teamLeaderId;
    
    private Integer annualLeaveBefore;

    @OneToMany(mappedBy = "team")
    private List<Employee> employeeList;

    public Team(String positionName) {
        this.teamName = positionName;
    }

    public void setTeamLeader(Employee leader) {
        this.teamLeader = leader;
    }
    // public void setTeamLeader(Integer leaderCode){
    //  this.teamLeaderId = leaderCode;
    // }
}

Team 엔티티 객체에서 기존에 단순히 팀장코드를 가지고있던 필드에 관계설정을 해줘서 직원객체랑 매핑해주었다.

이렇게 해주어야 Jpql에서 join을 사용해서 테이블끼리 조인해주는게 가능했다. 

Jpql문법에 대해서는 따로 공부해본적은없는데 관계설정해주는것만생각하면 

기본 sql문과 크게 차이없이 사용이가능한것같다.

 

이렇게 미니프로젝트 팀관련 api는 모두 처리해주었다. 

다음은 직원에 관련된 api기능을 처리해주자.

 

반응형
반응형

7일동안 과제를 마치고 이제 미니프로젝트 과제를 하게되었다. api 명세를 받아서 

api를 개발하면 되는데 지금 다 끝난시점에서 쓰는거지만 하나씩 할때마다 테이블에대해서도 고민하고 쿼리에대해서도 여러가지 생각을 해보게되는 좋은 경험이었다. 

 

[ 프로젝트 1단계 ]

SpringInitilizer로 스프링프로젝트를 생성한후에 실행해주었다.

 

웹으로 접근이 가능하게 하기위해서 spring web

Jpa를 쓰기위해서 spring Jpa

MySql 데이터베이스를 사용하기위해서 MySQL Driver

코드를 줄이고 편하게작성하기위해서 lombok을 의존성에 추가해서 프로젝트를 만들어주었다.

Dependencies추가

 

 

이제 필요한 기능명세에 따라서 기능을 만들어주겠다.

 

위의 기능을 보고 컨트롤러를 작성하기전에 DB를 먼저 생성해주었다. 

필요한 정보를보면 직원의정보 , 직원이 속한 부서 ,직원의 직급이 필요하다.

 

직원의 정보에 부서와,직급정보가 모두 포함될수도있지만 중복이 발생하기때문에 서로 분리해주었다.(정규화)

 

[직원]

직원코드, 직원 이름 , 직급(코드) , 부서(코드) , 생일 , 입사일

 

[직급]

직급코드, 직급이름

 

[부서]

부서코드, 부서이름 , 부서장(직원코드)

 

이렇게 간단하게 개념을 정리하고 테이블을 설계해주었다 코드는 기본키로설정하고

auto Increase로 자동증가하도록 해주었다. 

 

그리고 이런개념을 정리하기위해서 erd를 작성해보았다.

 

아래로가는 화살표는 이후 과제를 하면서 추가되는 테이블이다.

직급과 부서에는 해당하는 정보만 저장되도록하고 직원테이블에는 직급과 부서를 외래키로 참조하도록해서

직급의 이름과 부서의 이름을 받아올수있도록 해주었다. 

(사실 이게 제대로 정규화가 된건지는 잘모르겠다.)

이어진 선을보면 알수있는데 직급,부서 하나에 직원이 여러명 연결된 1:N관계이다.

 

이제 이 구조대로 자바코드에 DAO객체를 만들어주고 JpaRepository를 만들어주면 api를 만들 준비가 끝난다.

 

Employee(DAO)

@Entity
@Getter
@NoArgsConstructor
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "position_id")
    private Position position;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    private String startDate;
    private String birthDay;
}

 

@NoArgsConstructor어노테이션을 사용하면 파라미터가없는 기본생성자가 생성된다.( lombok )

EmployeeRepository(Repository)

public interface EmployeeRepository extends JpaRepository<Employee,Long> {
    Employee findByName(String name);
}

 

직원의 이름으로 값을 받아올수있는 추상메서드를 하나 작성해두었다.

 

Position(DAO)

@Entity
@NoArgsConstructor
@Getter
public class Position {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long positionId;
    private String positionName;
    @OneToMany(mappedBy = "position")
    private List<Employee> employeeList;
}

 

 

직급은 여러개의 직원정보를 가질수가있다.  (1:N)

예를 들어 직급이 대리 인데이터를 직원테이블에서조회한다고하면 김대리,이대리,박대리 가 나올수있는것이다.

그래서 직급하나가 여러개의 직원정보를 가져서 1:N관계가 되는것이다.

Jpa에서는 이런관계를 @OneToMany로풀어낼수있다.

이 기능을 이용하기위해서는 직급코드를 가지고있는

Employee클래스에서도 @ManyToOne으로 표기를 해주어야 서로 매핑이 되어서 이용이 가능하다. 

 

이 관계에는 주인이있는데 상대방을 알고있는쪽이 주인이라고 생각하면 편하다 

직원 클래스는 직급의코드를 가지고있어서 직급에대한 정보를 알고있지만 

직급 클래스는 직원에대해서 어떠한 정보도 가지고있지않다. 

 

그래서 직급클래스에는 mappedBy속성으로 관계의 주인을 명시해주고

직원 클래스에는 @JoinColumn을 사용해서 외래키인 position_id를 명시해주어야한다. - (쿼리에 들어갈 컬럼이름)

PositionRepository(Repository)

public interface PositionRepository extends JpaRepository<Position,Long> {
    Position findByPositionName(String positionName);
}

직급이름으로 조회할수있게 추상메서드를 작성해주었다.

 

Team(DAO)

@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long teamId;
    private String teamName;
    private Integer teamLeaderId;
    @OneToMany(mappedBy = "team")
    private List<Employee> employeeList;
}

 

TeamRepository(Repository)

public interface TeamRepository extends JpaRepository<Team,Long> {
    Team findByTeamName(String teamName);
}

부서이름으로 조회할수있게 추상메서드를 작성해주었다.

 

 

 

\main\resources\application.yml

spring:
  datasource:
    url: "jdbc:mysql://localhost:3308/company"
    username: "root"
    password: ""
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

리소스에 applocation으로 설정정보를 작성까지 해주었다.

 

이제 데이터베이스에 접근하기위한 준비가 모두 끝났다. 이제 API명세를 보고 필요한 기능을 하나씩 작성해보자.

반응형
반응형

 

FruitService

(기존코드)

@Service
public class FruitServiceImpl implements FruitService{

    private final FruitRepository repository;

    public FruitServiceImpl(@Qualifier("first")FruitRepository repository) {
        this.repository = repository;
    }

    @Override
    public void saveFruit(String name, String date, Integer price) {
        repository.saveFruit(name,date,price);
    }

    @Override
    public void updateFruit(long id) {
        if(repository.isFruitExist(id)){
            throw new IllegalArgumentException("수정할 자료가 없습니다.");
        }
        repository.updateFruit(id);
    }

    @Override
    public FruitShopAmoutResponse getFruitStat(String name) {
        return repository.getFruitStat(name);
    }
}

 

FruitRepository

(기존코드)

@Repository
public class FruitSQLRepository implements FruitRepository {

    private final JdbcTemplate template;

    public FruitSQLRepository(JdbcTemplate jdbcTemplate) {
        this.template = jdbcTemplate;
    }

    @Override
    public void saveFruit(String name, String date, Integer price) {
        String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
        template.update(sql, name, date, price);
    }

    @Override
    public void updateFruit(long id) {
        String sql = "update fruitshop set sell = true where id = ?";
        template.update(sql, id);
    }

    @Override
    public boolean isFruitExist(long id) {
        String readSql = "select id from fruitshop where id = ?";
        return template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
    }

    @Override
    public FruitShopAmoutResponse getFruitStat(String name) {
        String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";
        AtomicLong salesAmount = new AtomicLong(0);
        AtomicLong notSalesAmount = new AtomicLong(0);

        template.query(sql, new Object[]{name}, (rs, rowNum) -> {
            boolean status = rs.getBoolean("sell");
            long totalAmount = rs.getLong("totalAmount");

            if (status) {
                salesAmount.addAndGet(totalAmount);
            } else {
                notSalesAmount.addAndGet(totalAmount);
            }
            return null;
        });
        return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
    }
}

 

 

이전에 만들었던 SQL쿼리를 이용한  Repository와 Service를

Jpa를 이용한 케이스로 수정.

Fruit (DAO) - 테이블과 매칭되는 DAO객체에  @Entity와 @ID @Column으로 매칭

@Entity
public class Fruit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;
    @Column(nullable = false, length = 20)
    private String name;
    private Integer price; //타입 필드이름 이 테이블과 일치하면 @Column은 생략가능.
    private String wareHouseDate; // 테이블의 필드이름 ware_house_date
    private boolean sell = false;

    private static long seq = 1;

    public Fruit(String name, Integer price, String wareHouseDate) {
        this.name = name;
        this.price = price;
        this.wareHouseDate = wareHouseDate;
    }

    public Fruit() {

    }


    public String getName() {
        return name;
    }

    public long getPrice() {
        return price;
    }

    public String getWareHouseDate() {
        return wareHouseDate;
    }
    public void setSell(){
        this.sell = true;
    }
}

 

객체의 이름은 테이블과 매칭되어야하고

객체의 필드이름을 카멜케이스로 쓰게되면 테이블의 필드는 스네이크케이스로작성되어있어야한다.

 

만약 객체의 이름이 테이블의 이름과 다르다면 @Entity(name = "테이블이름") 으로 사용하면된다.

 

Repository (Interface)

public interface FruitRepositoryJpa extends JpaRepository<Fruit, Long> {
    //아래는 추상메서드 기본제공되는 메서드이외에 필요시 추가해서 사용이 가능하다.
    //메서드의 이름의로 쿼리가 자동으로 작성되어 구현되고
    //쿼리가 복잡하여 메서드의이름으로 구현이안되는경우 Query어노테이션을 통해서 쿼리를 사용할수있다.
    @Query(value = "SELECT sell, SUM(price) as totalAmount FROM fruit WHERE name = :name GROUP BY sell", nativeQuery = true)
    List<Object> findSellAndTotalAmountByName(@Param("name") String name);
    Long countByName(String name);
    List<Fruit> findByPriceGreaterThan(Integer price);
    List<Fruit> findByPriceLessThan(Integer price);
}

 

Interface로 repository를 만들어주고 JpaRepository를 상속받아준다. 

Jpa의 기본기능은 따로 상속받아서 오버라이딩 하지않아도.

save나 findById findAll같은 기본조회나 업데이트가 사용가능하다.

 

Service

@Service
public class FruitServiceJpa implements FruitService {

    private final FruitRepositoryJpa repository;
	
    //Jpa를 상속받은 repository를 주입받는다.
    public FruitServiceJpa(FruitRepositoryJpa repository) {
        this.repository = repository;
    }

    @Override
    public void saveFruit(String name, String date, Integer price) {
        repository.save(new Fruit(name, price, date));
    }

    @Override
    public void updateFruit(long id) {
        Fruit fruit = repository.findById(id).orElseThrow(IllegalArgumentException::new);
        fruit.setSell();
        repository.save(fruit);
    }

    @Override
    public FruitShopAmoutResponse getFruitStat(String name) {
        return mapToAmount(repository.findSellAndTotalAmountByName(name));
    }

    public Map<String, Long> getFruitCount(String name) {
        long count = repository.countByName(name);
        Map<String, Long> resultMap = new HashMap<>();
        resultMap.put("count", count);
        return resultMap;
    }

    private FruitShopAmoutResponse mapToAmount(List<Object> result) {
        AtomicLong salesAmount = new AtomicLong(0);
        AtomicLong notSalesAmount = new AtomicLong(0);
        result.stream()
                .map(obj -> (Object[]) obj)
                .forEach(rs -> {
                    boolean sell = (boolean) rs[0];
                    long totalAmount = ((BigDecimal) rs[1]).longValue();
                    if (sell) {
                        salesAmount.addAndGet(totalAmount);
                    } else {
                        notSalesAmount.addAndGet(totalAmount);
                    }
                });
        return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
    }

    public List<FruitResponse> getListCondition(String condition, Integer price) {
        if (condition.equals("GTE")) {
            // select * from fruit where price > 16000
            return repository.findByPriceGreaterThan(price).stream()
                    .map(fruit -> new FruitResponse(fruit.getName(), Integer.valueOf((int) fruit.getPrice()),
                            fruit.getWareHouseDate()))
                    .collect(Collectors.toList());
        } else{
            return repository.findByPriceLessThan(price).stream()
                    .map(fruit -> new FruitResponse(fruit.getName(), Integer.valueOf((int) fruit.getPrice()), fruit.getWareHouseDate()))
                    .collect(Collectors.toList());
        }
    }
}

 

Service에서 사용하는 repository를 Jpa를 상속받는 repositoryJpa로 주입받아서 

repository를 통해 Jpa의 기능을 사용할수있다. 

해당 인터페이스의 구현체를 따로구현하지않아도 구현체가 자동으로 생성되어주입되고

JpaRepository를 상속받으면서 빈으로도 자동등록되기때문에 따로 처리를 해줄필요도 없다. (간편)

 

기본 save나 update문 들은 Jpa에 구현되어있는 기능으로 만들수있었는데

getFruitStat은 조건과 그룹바이가 달려있어서 기본 쿼리만으로는 사용할수없어서

인터페이스에 따로 추상메서드를 만들어주었다.

@Query(value = "SELECT sell, SUM(price) as totalAmount FROM fruit WHERE name = :name GROUP BY sell", nativeQuery = true)
List<Object> findSellAndTotalAmountByName(@Param("name") String name);

 

@Query어노테이션 을 통해서 기존에 사용하던 쿼리문을 사용할수있다. 

name으로 조회해서 sell필드로 그룹을 만들어서 그룹의 가격을 List<Object>로 리턴한다.

 

@Override
public FruitShopAmoutResponse getFruitStat(String name) {
    return mapToAmount(repository.findSellAndTotalAmountByName(name));
}

private FruitShopAmoutResponse mapToAmount(List<Object> result) {
        AtomicLong salesAmount = new AtomicLong(0);
        AtomicLong notSalesAmount = new AtomicLong(0);
        result.stream()
                .map(obj -> (Object[]) obj)
                .forEach(rs -> {
                    boolean sell = (boolean) rs[0];
                    long totalAmount = ((BigDecimal) rs[1]).longValue();
                    if (sell) {
                        salesAmount.addAndGet(totalAmount);
                    } else {
                        notSalesAmount.addAndGet(totalAmount);
                    }
                });
        return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
    }

 

해당 쿼리를 통해서 List<Object>로 리턴되는값을 매핑해서

FruitShowAmountResponse객체로 만들어서 리턴해준다.

 

새로운 API를 추가하고 Jpa를 통해서 기능을 구현.

 

 

 

API 스펙

해당 API의 진입지점을 먼저 Controller에 만들어주자.

 

Controller

@GetMapping("/count") // api/v1/fruit 은 RequestMappint에서 명시되어있다.
public Map<String,Long> getFruitCount(@RequestParam String name){
    return service.getFruitCount(name);
}

 

"count": long인 Json을 리턴해주기위해서 Map타입을 리턴해주도록 했다.

 

Repository

public Map<String, Long> getFruitCount(String name) {
    long count = repository.countByName(name);
    Map<String, Long> resultMap = new HashMap<>();
    resultMap.put("count", count);
    return resultMap;
}

 

파라미터로 들어오는 이름으로 조회해서 갯수를 세는 쿼리를 만들어야하는데

Jpa는 interface에 있는 메서드의 이름으로 해당 쿼리를 구현해서 자동으로 만들어준다, 

Long countByName(String name); //select count(*) as count from fruit where name = ?

 

이렇게 인터페이스에 countByName이라는 함수를 만드는것만으로 기능을 구현할수있다. 

메서드의 이름을통해서 쿼리를 만드는거라고 생각하면 될것같다.

여기도 규칙이있는데 이 규칙은 필요할때 찾아보면서 익숙해지면 편하게 사용할수있을것같다. 

 

http://localhost:8080/api/v1/fruit/count?name=사과

 

 

이 문제도 Jpa를 통해서 쿼리를 만들지않고 추상메서드를 만드는것으로 해결할수있었다.

여기서는 조건에 따라 다른 쿼리문을 실행시켜줘야한다.

SQL문이었다면 쿼리문을 두개만들어놓고 조건에 따라 다른 쿼리를 실행시켜주거나

쿼리문 중간의 조건을 바꿔치기 하는방법으로 사용할수있을것같다.

Jpa에서는 쿼리문을 만들지않아도되기때문에 간단하게 추상 메서드를 두개 만들어서 사용해주었다.

 

repositoryJpa

List<Fruit> findByPriceGreaterThan(Integer price);
List<Fruit> findByPriceLessThan(Integer price);

 

Service

if (condition.equals("GTE")) {
    // select * from fruit where price > 16000
    return repository.findByPriceGreaterThan(price).stream()
            .map(fruit -> new FruitResponse(fruit.getName(), Integer.valueOf((int) fruit.getPrice()),
                    fruit.getWareHouseDate()))
            .collect(Collectors.toList());
} else{
    return repository.findByPriceLessThan(price).stream()
            .map(fruit -> new FruitResponse(fruit.getName(), Integer.valueOf((int) fruit.getPrice()), fruit.getWareHouseDate()))
            .collect(Collectors.toList());
}

 

이렇게 추상메서드를 두개만들고

service에서 문자열로들어오는조건에 따라서 다른 메서드를 실행하게 처리해서 적용해주었다. 

 

Controller

@GetMapping("/list")
public List<FruitResponse> getListCondition(String condition, Integer price){
    return service.getListCondition(condition,price);
}

 

 

Jpa를 새로 적용하면서 중간에 에러도 많이 발생했는데 거의 대부분이

객체의 필드와 조회된 테이블이 매칭되지않아서 생기는 문제였다. 

이후에 익숙해지고 컨버터 개념도 배우면 좀 쉽게 해결이가능할것같지만 우선은 지금사용하는정도로도 

코드의 양을 눈에 띄게 줄일수있고 보는걸로 db에서 어떤기능을 하는지 쉽게 파악할수있게되었다.

반응형
반응형

4일차에 만든 api를 분리해서 리팩토링 해야한다. 

@RestController
@RequestMapping("/api/v1/fruit")
public class FruitShopController {
    private final JdbcTemplate template;

    public FruitShopController(JdbcTemplate template) {
        this.template = template;
    }

    @PostMapping
    public void saveFruit(@RequestBody FruitSaveRequest request) {
        String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
        template.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
    }

    @PutMapping
    public void sellFruit(@RequestBody Map<String, Object> requestJson) {
        long id = Long.parseLong(requestJson.get("id").toString());
        String readSql = "select id from fruitshop where id = ?";
        Boolean isUserNotExist = template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
        if (isUserNotExist) {
            throw new IllegalArgumentException("수정할 자료가 없습니다.");
        }

        String sql = "update fruitshop set sell = true where id = ?";
        template.update(sql, id);
    }

    @GetMapping("/stat")
    public FruitShopAmoutResponse getFruitStat(@RequestParam("name") String name) {
        String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";

        AtomicLong salesAmount = new AtomicLong(0);
        AtomicLong notSalesAmount = new AtomicLong(0);

        template.query(sql, new Object[]{name}, (rs, rowNum) -> {
            boolean status = rs.getBoolean("sell");
            long totalAmount = rs.getLong("totalAmount");

            if (status) {
                salesAmount.addAndGet(totalAmount);
            } else {
                notSalesAmount.addAndGet(totalAmount);
            }
            return null;
        });

        return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
    }
}

 

문제

각 메서드마다 여러개의 역할을 가지고있는걸알수있다.

Controller에서는 api의 진입지점과 어떤 동작을 하는지 명확하게 알수있게 정리하고

SQL문은 repository에서 처리하고

에러처리 나 추가적인 로직은 service에서 처리할수 있도록 메서드를 계층별로 분리해야한다.

 

각 메서드를 분리한 예시

 

한 개로 되어있는 메서드를

SQL부분은 repostory에서

에러처리와 repository의 메서드 호출은 service에서 

Controller 에서는 service 계층의 메서드만 호출해준다. 

 

완성 코드

Controller

@RestController
@RequestMapping("/api/v1/fruit")
public class FruitShopController {

    private FruitService service;

    public FruitShopController(FruitService service) {
        this.service = service;
    }

    @PostMapping
    public void saveFruit(@RequestBody FruitSaveRequest request) {
        service.saveFruit(request.getName(),request.getWarehousingDate(),request.getPrice());
    }

    @PutMapping
    public void updateFruit(@RequestBody Map<String, Object> requestJson) {
        long id = Long.parseLong(requestJson.get("id").toString());
        service.updateFruit(id);
    }

    @GetMapping("/stat")
    public FruitShopAmoutResponse getFruitStat(@RequestParam("name") String name) {
       return service.getFruitStat(name);
    }
}

 

Service

public interface FruitService {
    void saveFruit(String name, String date, Long price);
    void updateFruit(long id);
    FruitShopAmoutResponse getFruitStat(String name);
}
@Service
public class FruitServiceImpl implements FruitService{

    private final FruitRepository repository;

    public FruitServiceImpl(FruitRepository repository) {
        this.repository = repository;
    }

    @Override
    public void saveFruit(String name, String date, Long price) {
        repository.saveFruit(name,date,price);
    }

    @Override
    public void updateFruit(long id) {
        if(repository.isFruitExist(id)){
            throw new IllegalArgumentException("수정할 자료가 없습니다.");
        }
        repository.updateFruit(id);
    }

    @Override
    public FruitShopAmoutResponse getFruitStat(String name) {
        return repository.getFruitStat(name);
    }
}

Repository

public interface FruitRepository {
    void saveFruit(String name,String date,Long price);

    void updateFruit(long id);

    boolean isFruitExist(long id);
    FruitShopAmoutResponse getFruitStat(String name);
}
@Repository
public class FruitSQLRepository implements FruitRepository {

    private final JdbcTemplate template;

    public FruitSQLRepository(JdbcTemplate jdbcTemplate) {
        this.template = jdbcTemplate;
    }

    @Override
    public void saveFruit(String name, String date, Long price) {
        String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
        template.update(sql, name, date, price);
    }

    @Override
    public void updateFruit(long id) {
        String sql = "update fruitshop set sell = true where id = ?";
        template.update(sql, id);
    }

    @Override
    public boolean isFruitExist(long id) {
        String readSql = "select id from fruitshop where id = ?";
        return template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
    }

    @Override
    public FruitShopAmoutResponse getFruitStat(String name) {
        String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";
        AtomicLong salesAmount = new AtomicLong(0);
        AtomicLong notSalesAmount = new AtomicLong(0);

        template.query(sql, new Object[]{name}, (rs, rowNum) -> {
            boolean status = rs.getBoolean("sell");
            long totalAmount = rs.getLong("totalAmount");

            if (status) {
                salesAmount.addAndGet(totalAmount);
            } else {
                notSalesAmount.addAndGet(totalAmount);
            }
            return null;
        });
        return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
    }
}

 

이제 컨트롤러는 api의 진입점 역활을 하고 어떤동작을 하는지 한눈에 파악하기 쉬워졌다.

그리고 JdbcTemplate을 의존하지않고 Service클래스만 의존해서 사용하게된다. 

 

스프링 컨테이너 & 스프링 빈

각 계층에서 생성자를 주입받을때 인스턴스를 생성하지 않고 자동으로 생성이 되는걸 알수있다. 

이는 스프링에서 지원하는 스프링 컨테이너와 스프링 빈 덕분이다.

 

각 클래스의 위에 붙어있는 @RestController , @Service , @Repostory 덕분에

스프링은 이 객체들을 빈으로 인식하고 시작되는시점에 컨테이너에 이 객체들을 생성해서 보관하게된다

이로 인해서 필요할때에 객체를 생성하지않고 자동으로 주입해서 사용할수있다.

 

JdbcTemplate객체의 경우에는 스프링에서 기본으로 등록되어있는 빈이기때문에 따로 설정을 해주지않아도 주입받아서 사용할수있다 이를 의존성주입 (Dependency Injection) 이라고 한다. 

 

나는 서비스와 저장소 객체를 인터페이스로 만들고 그 구현체를 만드는 방법으로 사용했는데

이렇게 하면 Controller나 Service에서 생성자에 주입받을때에 interface객체를 타입으로 넣게되면

스프링컨테이너에서 구현체인 빈을 자동으로 주입해주기 때문에 추후에 구현체를 변경해야할일이 생겼을때

구현체의 코드만 수정해주면 Service계층이나 Controller계층의 코드는 수정하지 않고

자동으로 구현체가 변경되어서 주입된다. 

 

ex) MemoryRepository

@Primary
@Repository
public class FruitMemoryRepository implements FruitRepository{
    List<Fruit> fruitRepository = new ArrayList<>();

    @Override
    public void saveFruit(String name, String date, Long price) {
        fruitRepository.add(new Fruit(name,price,date));
    }

    @Override
    public void updateFruit(long id) {
        fruitRepository.set((int) id,new Fruit("수정",000,"00-00-00"));
    }

    @Override
    public boolean isFruitExist(long id) {
        if(fruitRepository.get((int)id) == null){
            return false;
        }
        return true;
    }

    @Override
    public FruitShopAmoutResponse getFruitStat(String name) {
        return new FruitShopAmoutResponse(0,fruitRepository.stream().mapToLong(Fruit::getPrice).sum());
    }
}

 

이렇게 Repository의 구현체를 하나 더 만들고 @Repository 어노테이션을 붙여주면

스프링에서는 어떤 구현체를 주입해야할지 몰라서 에러가 발생하게된다.

이럴때는 @Primary어노테이션을 주입할 객체에 붙여서 우선순위를 만들어주거나 

@Qulifier 어노테이션을 이용해서 주입할 빈의 이름을 지정하고

주입받는곳에서 파라미터에 @Qualifier 어노테이션을 같이붙여서 가져오고싶은 빈의 이름을 넣어서 받아올수도있다. 

 

@Qualifier("first") // 빈의 이름을 설정
@Repository
public class FruitMemoryRepository implements FruitRepository{


@Service
public class FruitServiceImpl implements FruitService{

    private final FruitRepository repository;

    public FruitServiceImpl(@Qualifier("first")FruitRepository repository) { //주입시 이름으로 지정해서 받아옴
        this.repository = repository;
    }

 

반응형
반응형

여기서는 요청받을 객체와 응답을 보낼 객체를 만들어서 파라미터를 받고 응답을 내보내줬다.

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;
    }
}

이 객체는 파라미터를 받기위한 객체인데 강의중에 만든 객체지만

숫자 파라미터를 두개받는 객체로 적절해보여서 그냥 재사용했다.

 

public class CalcResult {
    private int add;
    private int minus;
    private int multiply;

    public CalcResult(CalculatorAddRequest request) {
        this.add = request.getNumber1() + request.getNumber2();
        this.minus = request.getNumber1() - request.getNumber2();
        this.multiply = request.getNumber1() * request.getNumber2();
    }

    public int getAdd() {
        return add;
    }

    public int getMinus() {
        return minus;
    }

    public int getMultiply() {
        return multiply;
    }
}

이 객체는 계산결과를 응답으로 내보내기 위해서 새로 생성한 객체이다.

생성시에 CalculatorAddRequest객체를 받도록 만들었는데

의존을 줄이려면 그냥 int타입 파라미터 두개로 받아서 만드는것도 괜찮아보인다. 

그리고 처음에는 필드와 생성자만 만들어서 사용했는데 요청을 보내보니 응답이 에러가 나는 상황이 생겼다. 

찾아보니 getter메서드가 있어야 결과를 받아서 직렬화하여 Json객체로 응답을 보낼수있다는듯 하다.

 

@GetMapping("/api/v1/calc")
public CalcResult returnResult(CalculatorAddRequest request) {
    return new CalcResult(request);
}

 

이렇게 응답객체와 요청객체를 사용해서 컨트롤러를 만들었다. 

 

요청에 따른 응답이 적절하게 잘 오고있다.

 

여기서는 LocalDate클래스 를 이용해서 만들었다.

해당 객체에대해 아는게 없어서 조금 헤매기는 했지만 간단하게 구현할수있었다.

 

@GetMapping("/api/v1/dayOfWeek")
public String calcDate(String date){
    int[] dateArray = parseDate(date);
    LocalDate localDate = LocalDate.of(dateArray[0],dateArray[1],dateArray[2]);
    return localDate.getDayOfWeek().toString();
}

private int[] parseDate(String dateString) {
    return Arrays.stream(dateString.split("-"))
            .mapToInt(Integer::parseInt)
            .toArray();
}

 

LocalDate 객체는 생성자가 private으로 되어있어서 new LocalDate( ) 이런방법으로는 객체를 생성해서 날짜 파라미터를 넣는게 불가능했다. 그래서 찾아보니 .of라는 메서드를 통해서 객체에 파라미터를 넣어줄수있어서 이 방법을 사용했다.

 

아래의 메서드는 날짜데이터를 String으로 받아서 구분자로 연 , 월 ,일  을 구분해서 배열에 넣어주는 메서드이다. 

이렇게 하면 LocalDate에 날짜파라미터를 넣고 메서드를통해서 요일을 쉽게 출력해줄수있다.

실제  23년1월1일은 일요일 이었따..

 

여기서는 좀 시행착오가있었다. 

@PostMapping("/api/v1/arraySum")
public Integer arraySum(@RequestBody List<Integer> numbers){
    return numbers.getNumbers().stream().mapToInt(Integer::intValue).sum();
}

처음에는 객체를 따로 생성하지않기위해서 List<Integer> 를 파라미터로 넣어서 시도해봤는데 에러가 발생했다.

 

그래서 따로 요청 객체를 생성해주었다.

public class CalculatorArrayRequest {
    private final List<Integer> numbers = new ArrayList<>();

    public List<Integer> getNumbers() {
        return numbers;
    }
}

해당 리스트를 필드로 가지는 객체를생성하고 그 객체를 받아오기위해서 getter메서드만 넣어주었다.

//객체로 파라미터를 받도록 변경
@PostMapping("/api/v1/arraySum")
public Integer arraySum(@RequestBody CalculatorArrayRequest numbers){
    return numbers.getNumbers().stream().mapToInt(Integer::intValue).sum();
}

 

 

찾아보니 Json으로 오는값을 받기위한 객체가 필요하고 객체없이 쓰려면

HttpEntity라는 클래스를 이용해서 Json으로 오는 값을 받아야하는데 그 값을 받은후에

파싱하고 연산하는 과정을 생각하면 객체를 만들어서 그 과정을 자동화 시켜주는게 훨씬 이득인것같았다 .. 

 

스프링에서 자체적으로 해주는 자동화 기능의 혜택을 받기위해서는 지켜야할게 많지만

지켰을때에 득이 크다는 생각이 들었다 

반응형
반응형

[질문]

  • 어노테이션을 사용하는 이유 (효과) 는 무엇일까?
  • 나만의 어노테이션은 어떻게 만들 수 있을까?

 

자바에는 3종류의 어노테이션 이 있다.

 

표준 어노테이션 - 자바에서 기본적으로 제공하는 기능인 어노테이션

메타 어노테이션 - 어노테이션을 만들기위한 어노테이션

사용자정의 어노테이션 - 사용자가 직접 생성한 어노테이션

 

이런 어노테이션들은 주석처럼 사용자가 읽었을때 의미를전달할수도있지만

프로그램이 이 어노테이션을 읽어서 특정 어노테이션 마다 다른 처리를 하도록 할수도있다.

 

어노테이션을 찍었을때 해당 메서드나 클래스에 로그를 찍도록 처리할수도있고 특정과정을 추가할수도 없앨수도 있다.

이런 점에서는 코드에 마킹을 해놓는 기능이라고 생각한다. 

반응형
반응형

클린코드 작성하기 

public class Main {
    public static void main(String[] args) {
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        int a = scanner.nextInt();
        
        int r1 = 0, r2 = 0, r3=0, r4=0, r5 = 0, r6 = 0;
        
        for(int i = 0; i < a; i++){
            double b = Math.random() * 6;
            if(b >=0 && b <1){
                r1 ++;
            }else if(b >= 1 && b < 2){
                r2 ++;
            }else if(b >= 2 && b < 3){
                r2 ++;
            }else if(b >= 3 && b < 4){
                r2 ++;
            }else if(b >= 4 && b < 5){
                r2 ++;
            }else if(b >= 5 && b < 6){
                r2 ++;
            }
        }

        System.out.printf("1은 %d번 나왔습니다.\n",r1);
        System.out.printf("2은 %d번 나왔습니다.\n",r2);
        System.out.printf("3은 %d번 나왔습니다.\n",r3);
        System.out.printf("4은 %d번 나왔습니다.\n",r4);
        System.out.printf("5은 %d번 나왔습니다.\n",r5);
        System.out.printf("6은 %d번 나왔습니다.\n",r6);
    }
}

 

이 코드를 리팩토링 하는게 5일차 미션이다. 

 

코드에서 역활을 분리해서 메서드를 추출.

public class TestClass {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int a = getA(scanner);

        int r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0;

        condition(a, r1, r2, r3, r4, r5, r6);
        print(r1, r2, r3, r4, r5, r6);
    }
	
    //주사위를 던질횟수를 입력받는다
    private static int getA(Scanner scanner) {
        System.out.println("숫자를 입력하세요 : ");
        return scanner.nextInt();
    }
	
    //주사위를 던져 1~6까지의 숫자가 몇번나왔는지 증가
    private static void condition(int a, int r1, int r2, int r3, int r4, int r5, int r6) {
        for (int i = 0; i < a; i++) {
            double b = Math.random() * 6;

            switch ((int) b) {
                case 0:
                    r1++;
                    break;
                case 1:
                    r2++;
                    break;
                case 2:
                    r3++;
                    break;
                case 3:
                    r4++;
                    break;
                case 4:
                    r5++;
                    break;
                case 5:
                    r6++;
                    break;
                default:
                    // 기본값 처리
                    break;
            }
        }
    }
	
    //주사위 결과 출력
    private static void print(int r1, int r2, int r3, int r4, int r5, int r6) {
        System.out.printf("1은 %d번 나왔습니다.\n", r1);
        System.out.printf("2은 %d번 나왔습니다.\n", r2);
        System.out.printf("3은 %d번 나왔습니다.\n", r3);
        System.out.printf("4은 %d번 나왔습니다.\n", r4);
        System.out.printf("5은 %d번 나왔습니다.\n", r5);
        System.out.printf("6은 %d번 나왔습니다.\n", r6);
    }
}

 

전체적인 코드는 길어졌지만 메인 메서드만 보면 이전보다는 가독성이 좋아지고

메서드의 이름을 통해 어떤동작을 하는지 간단하게 파악이 가능해졌다. 

 

이제 중복되는 과정들은 조건문과 반복문으로 줄이고 주사위의 횟수를 저장할값도 더 쉽게 만들어보자.

public class TestClass {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Map<Integer, Integer> dice = createDice(6);

        int count = getCount(scanner);
        play(dice,count);
        print(dice);
    }

    private static Map<Integer, Integer> createDice(int a) {
        Map<Integer, Integer> dice = new HashMap<>();
        for (int i = 0; i < a; i++) {
            dice.put(i + 1, 0); // key(주사위번호) : value(해당번호가 나온 횟수)
        }
        return dice;
    }

    private static int getCount(Scanner scanner) {
        System.out.println("숫자를 입력하세요 : ");
        return scanner.nextInt();
    }

    private static void condition(Map<Integer, Integer> dice,int count) {
        for (int i = 0; i < count; i++) {
            int b = (int) (Math.random() * dice.size()) + 1;

            dice.put(b, dice.getOrDefault(b, 0) + 1);
        }
    }

    private static void print(Map<Integer, Integer> dice) {
        for (Entry<Integer, Integer> entry : dice.entrySet()) {
            System.out.printf("%d은 %d번 나왔습니다.\n", entry.getKey(),entry.getValue());
        }
    }
}

 

중간과정을 좀 많이 건너뛴 최종 리팩토링 코드이다. 

 

1.  r1 , r2 로 주사위의 눈금을 받아주던 변수를 확장성있게 map으로 변경


int r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0; 
//이제 눈금갯수를 수정해도 변수의 갯수가 바뀌지 않고 변수를 여러개 선언하지않아도 된다.
Map<Integer, Integer> dice = createDice(6);

//Array나 배열을 사용하면 이 메서드를 생성하지 않아도 된다.
private static Map<Integer, Integer> createDice(int a) {
        Map<Integer, Integer> dice = new HashMap<>();
        for (int i = 0; i < a; i++) {
            dice.put(i + 1, 0); // key(주사위번호) : value(해당번호가 나온 횟수)
        }
        return dice;
    }

 

맵말고 Array나 그냥 일반배열로 해도 index를 통해서 눈금을 나타낼수있어서 그쪽이 더 간단할것같다. 

이번에는 그냥 Map타입을 사용해보고싶어서 Map으로 주사위를 지정했지만

이후 다른 기능이 추가될때 특성에따라 타입을 수정해야 할수도 있을것같다.

 

2.조건 부분을 반복문과 조건문으로 수정

private static void condition(int a, int r1, int r2, int r3, int r4, int r5, int r6) {
        for (int i = 0; i < a; i++) {
            double b = Math.random() * 6;

            switch ((int) b) {
                case 0:
                    r1++;
                    break;
                case 1:
                    r2++;
                    break;
                case 2:
                    r3++;
                    break;
                case 3:
                    r4++;
                    break;
                case 4:
                    r5++;
                    break;
                case 5:
                    r6++;
                    break;
                default:
                    // 기본값 처리
                    break;
            }
        }
    }
// if else조건문을 switch문으로 변경했을때 가독성은 더 좋아졌지만 반복되고 코드가 길어지는 문제가있었다
// 이를 반복과 조건문으로 변경하고 r1,r2같이 파라미터가 여러개이던 문제도 dice를 Map타입으로 만들면서 해결되었다.
// a라는 이름으로 들어오던 파라미터도 의미를 알기쉽게 count라는 이름으로 변경해주었다.
private static void play(Map<Integer, Integer> dice,int count) {
        for (int i = 0; i < count; i++) {
            int b = (int) (Math.random() * dice.size()) + 1;

            dice.put(b, dice.getOrDefault(b, 0) + 1);
        }
    }

이제 누가봐도 파라미터로 받는 count만큼 주사위를 굴려서 dice에 매칭 시켜주는 함수라는 걸 알기 쉬워졌다.

메서드이름은 condition보다는 주사위를 굴린다는 의미로 바꾸어주는게 좋을것같다. condition => play

 

3.출력부분을 반복문으로 수정

//주사위 결과 출력
    private static void print(int r1, int r2, int r3, int r4, int r5, int r6) {
        System.out.printf("1은 %d번 나왔습니다.\n", r1);
        System.out.printf("2은 %d번 나왔습니다.\n", r2);
        System.out.printf("3은 %d번 나왔습니다.\n", r3);
        System.out.printf("4은 %d번 나왔습니다.\n", r4);
        System.out.printf("5은 %d번 나왔습니다.\n", r5);
        System.out.printf("6은 %d번 나왔습니다.\n", r6);
    }
    
    private static void print(Map<Integer, Integer> dice) {
        for (Entry<Integer, Integer> entry : dice.entrySet()) {
            System.out.printf("%d은 %d번 나왔습니다.\n", entry.getKey(),entry.getValue());
        }
    }

 

이전에는 출력해주던 메서드도 반복되고 파라미터가 많아서 주사위의 눈금이 늘어나면 그만큼 파라미터도 늘어나야하고 코드도 길어져야 하는 문제가 있었다. 이것 또한 dice로 타입이 변경되면서 반복문이 사용가능해면서 해결되었다.

 

이 부분은 주사위를 map, 배열 , Array로 변경하면서 해결했다.

주사위 자체를 따로 Dice클래스로 만들고 그 안에 주사위의 눈금과 몇번나왔는지 지정해주는 필드를 생성해도 될것같고 

더 다양한 방법도 있을것같다.

 

<클린코드>

이 개념에 대해서 배워보았는데 이전에 우테코를 하면서 코드 컨벤션이라는 개념을 배워본적이있었다.

코드를 작성하기위한 약속이었는데 코드 컨벤션만 준수하면서 작성해도 어느정도 가독성이 좋고

모두가 이해하기 쉬운 코드를 작성할수있다고 생각한다. 가장 좋은 코드는 책 처럼 읽어지는 코드가 아닐까?

반응형
반응형

4일차 과제는 API를 직접만들어보는 과제였다.

 

문제1

우리는 작은 과일 가게를 운영하고있습니다. 과일 가게에 입고된 "과일 정보"를 저장하는 API를 만들어 봅시다.

 

API 스펙

 

위의 요구사항을 보고 우선 값을 저장할수있는 테이블을 생성해주고 

요청값을 파라미터로 받아줄 객체를 생성했다. 

 

테이블 생성

CREATE TABLE fruitshop (
                           id int PRIMARY KEY AUTO_INCREMENT,
                           name VARCHAR(50),
                           warehousedate DATETIME,
                           price INT
);

//이후 필드 추가 , 과일의 판매여부를 체크할 필드
ALTER TABLE fruitshop
    ADD sell BOOLEAN DEFAULT false;

 

파라미터를 전달해줄 DTO객체

public class FruitSaveRequest {

    private String name;
    private Long price;
    private String warehousingDate;

    public String getName() {
        return name;
    }

    public Long getPrice() {
        return price;
    }

    public String getWarehousingDate() {
        return warehousingDate;
    }
}

 

아직 DAO와 DTO의 기준이 모호하다 . 해당객체는 파라미터를받아서 insert 쿼리에 넣어주기위한 용도로만 사용하므로  DTO라고 생각한다.

 

컨트롤러 코드 

@RestController
@RequestMapping("/api/v1/fruit")
public class FruitShopController {
    private final JdbcTemplate template;

    public FruitShopController(JdbcTemplate template) {
        this.template = template;
    }

    @PostMapping
    public void saveFruit(@RequestBody FruitSaveRequest request){
        String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
        template.update(sql,request.getName(),request.getWarehousingDate(),request.getPrice());
    }
}

 

 

json으로 들어오는 파라미터가 FruitSaveRequest에 매칭되어서 saveFruit메서드에 파라미터로 들어가게되고 

template.update 에서 sql의 파라미터로 사용된다.

 

*자바에서 정수를 다루는 가장 대표적인 두 가지 방법은 int 와 long 이다. 

이 두가지 방법 중 위 API에서 long을 사용한 이유는 무엇일까?

 

값이나 id값은 int자료형으로는 커버할수있는 범위가 작기때문에 값이 얼마나 들어올지 예측할수없는 db의 경우에 커버리지를 넓게 잡기위해 long타입으로 지정해주는듯 하다.

 

문제2

과일이 팔리게 되면, 우리 시스템에 팔린 과일 정보를 기록해야 합니다.

 

API스펙

 

id를 파라미터로 받아서 조건에 맞는 필드를 업데이트 해주면 될것같다.

@PutMapping
    public void sellFruit(@RequestBody Map<String, Object> requestJson){
        long id = Long.parseLong(requestJson.get("id").toString()); //id값
        String readSql = "select id from fruitshop where id = ?";  // 값이 있는지 확인하기위한 쿼리
        Boolean isUserNotExist = template.query(readSql, (rs, rowNum) -> 0, id).isEmpty(); //(rs, rowNum) -> 0 결과값이 있는 경우 0을 반환  
        if(isUserNotExist){
            throw new IllegalArgumentException("수정할 자료가 없습니다.");
        }

        String sql = "update fruitshop set sell = true where id = ?";
        template.update(sql,id);
    }

 

해당하는 아이디의 값이있는지 검증하고 값이 있으면 해당 값의 sell을 true로 변경해준다. (판매되었다는 의미)

 

여기에서는 값을 받기위해서 객체를 생성하지않고 Map을통해서 들어오는 Json을 받아서 long타입의 id로 변경해주었다.

 

 

문제3

우리는 특정 괴일을 기준으로 팔린 금액, 팔리지 않은 금액을 조회하고 싶습니다.

 

API스펙

@GetMapping("/stat")
    public FruitShopAmoutResponse getAmount(@RequestParam String name){
        String sql = "SELECT id,name,warehousedate,price,sell FROM fruitshop where name = ? ";

        List<FruitShopResponse> query = template.query(sql,new Object[]{name}, (rs, rowNum) -> {
            System.out.println("rs = " + rs);
            long id = rs.getLong("id");
            String fruitname = rs.getString("name");
            String warehouseDate = rs.getString("warehouseDate");
            long price = rs.getLong("price");
            boolean sell = rs.getBoolean("sell");
            return new FruitShopResponse(id, fruitname, warehouseDate, price, sell);
        });

        FruitShopAmoutResponse fruitShopAmoutResponse = getFruitShopAmoutResponse(query);
        return fruitShopAmoutResponse;
    }
    
    private static FruitShopAmoutResponse getFruitShopAmoutResponse(List<FruitShopResponse> result) {
        long salesAmount = result.stream()
                .filter(rs -> rs.getSell())
                .mapToLong(rs -> rs.getPrice())
                .sum();

        long notSalesAmount = result.stream()
                .filter(rs -> !rs.getSell())
                .mapToLong(rs -> rs.getPrice())
                .sum();
        FruitShopAmoutResponse response = new FruitShopAmoutResponse(salesAmount, notSalesAmount);
        return response;
    }
반응형

+ Recent posts