1. CleanCode
- ‘클린코드’(로버트 C. 마틴) 참고
1) 클린코드 규칙
- 의도를 분명히 밝혀라.
- 어떤 데이터를 저장하고 있는지 예측
- 서로 흡사한 이름을 사용하지 않도록 주의 하자
- 그릇된 정보를 피하라.
- 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용하지 말자
- 의미 있게 구분 하라.
- 발음하기 쉬운 이름으로 정하자.
- 우리는 혼자 일하지 않는다.
- 검색하기 쉬운 이름을 사용하라.
- 타입과 관련된 문자열을 넣지 말아라.
- 한 개념에 한 단어를 사용하라.
- 일관성 있는 어휘를 선택해서 이름을 붙이자.
- ex) Controller 계층에서는
fetch
, Service 계층에서는get
라고 같은 역할을 가진 메서드명으로 혼동하여 사용한다.
- 의미 있는 맥락을 추가하라.
- 불필요한 맥락을 없애라.
- 의미 없는 접두사 붙이지 X
- 의미 전달이 제대로 안되는 짧은 변수명
2) 추가적인 개념
- 클린한 코드 = 한 사람이 짠 것처럼 짜야 한다.
- 컨벤션의 필요성!
3) 함수 설계법
- 한 가지만 하자
a. 함수는 최대한 작게 만들기
- switch문을 이용하여 런타임 때, 함수 호출이 정해지도록 추상 메서드로 따로 빼기
- 하지만, 이렇게 설계하면, 복잡성 증가
- 오류 처리는 항상 예외 처리로 처리하고 예외처리를 하기 위해서 보통, try-catch 문을 이용하기!
- 컨트롤단에서 try catch, 글로벌로 예외처리 사용 안 하면, 서비스에서 throws 사용
- 예외 처리를 위해 ENUM 클래스 이용하기!
b. 함수의 역할을 명령과 조회를 분리시켜라!
c. 객체에서 캡슐화는 필수이다. 변경의 전파를 최대한 줄이기 위해 필요하다.
- getter의 사용성도 최대한 줄여야 한다.(setter에 못지 않다.)
- why? 예를 들어, Audience 객체에서 Bag 객체를 사용해야할 때, getBag()을 사용하는 것은 불가능해야 한다. Bag의 이름 등이 달라지면 모든 소스코드에 들어가서 변경해야 하기 때문이다.
4) 설계 예시
a. 수정 방향
-
다음 코드를 코드를 읽는 사람이 쉽게 이해할 수 있고 예측할 수 있도록 수정해보자!
-
다음 코드를 비즈니스 요구사항의 변경에 유연하고 기능 확장성을 가지는 코드로 수정해보자!
b. 예제 코드(수정 전)
a) Entity 모음 + 비즈니스 로직
- Invitation.java
public class Invitation {
private LocalDateTime when;
}
- Ticket.java
public class Ticket {
private Long fee;
public Ticket(Long fee) {
this.fee = fee;
}
public Long getFee() {
return fee;
}
}
- Audience.java
public class Audience {
private final Bag bag;
public Audience(Bag bag){
this.bag = bag;
}
public long buy(Ticket t){
return bag.hold(t);
}
}
- Bag.java
public class Bag {
private Long amount;
private final Invitation invitation;
private Ticket ticket;
public Bag(long amount) {
this(null, amount);
}
public Bag(Invitation invitation, long amount) {
this.invitation = invitation;
this.amount = amount;
}
public Long hold(Ticket ticket) {
if (hasInvitation()) {
setTicket(ticket);
return 0L;
} else {
setTicket(ticket);
minusAmount(ticket.getFee());
return ticket.getFee();
}
}
private boolean hasInvitation() {
return invitation != null;
}
private boolean hasTicket() {
return ticket != null;
}
private void setTicket(Ticket ticket) {
this.ticket = ticket;
}
private void minusAmount(long amount) {
this.amount -= amount;
}
private void plusAmount(long amount) {
this.amount += amount;
}
}
- Theater.java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class Theater {
public void enter(Audience audience, TicketSeller ticketSeller){
long ticketFee = ticketSeller.sellTo(audience);
ticketSeller.receivePay(ticketFee);
}
}
- TicketOffice.java
import java.util.Arrays;
import java.util.List;
public class TicketOffice {
private long amount;
private final List<Ticket> tickets;
public TicketOffice(Long amount, Ticket ... tickets) {
this.amount = amount;
this.tickets = Arrays.asList(tickets);
}
public Ticket publishTicket(){
return getTicket();
}
public void increaseSalesAmount(long amount){
plusAmount(amount);
}
public Ticket getTicket(){
return tickets.get(0);
}
public void minusAmount(long amount) {
this.amount -= amount;
}
private void plusAmount(long amount) {
this.amount += amount;
}
}
- TicketSeller.java
public class TicketSeller {
private final TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public long sellTo(Audience a) {
return a.buy(ticketOffice.publishTicket());
}
public void receivePay(long ticketFee){
ticketOffice.increaseSalesAmount(ticketFee);
}
}
b) Controller, Service 단
- TheaterController.java
import com.cafe.service.CafeService;
import com.theater.service.TheaterService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
@RequestMapping("/theater")
@RequiredArgsConstructor
public class TheaterController {
private final TheaterService theaterService;
@GetMapping("hello")
public String welcomeMessage(){
return "Welcome to The Wanted Theater";
}
@GetMapping("enter")
public String enter(){
return theaterService.enter();
}
}
- TheaterService.java
import com.theater.service.handler.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class TheaterService {
private final Theater theater;
public String enter(){
theater.enter(new Audience(new Bag(1000L)),
new TicketSeller(new TicketOffice(20000L, new Ticket(100L))));
return "Have a good time.";
}
}
c. 예제 코드(수정 후)
- Bag.java
- TicketSeller.java
2. OOP
- ‘오브젝트’(조영호) 참고
0) 들어가기 전 및 실습 코드
- 객체지향은 객체만 존재하면 의미가 없다. 메시지를 주고받아야 진짜 의미가 있는것이다.
- UUID로 타입을 쓰는 이유??(ArrayList나 List를 쓰지 않는 이유?) 또는 switch문의 type에서 enum을 사용하는 이유?
- 열거형 타입인 ENUM을 쓰는 이유 ? 사용자 편의성으로서 타입을 봐주는 것이다! 또한 데이터 타입이 정해지면, 내부코드가 바뀌면서 변수 타입도 전부 다 바꾸어야 하므로!
- Ex) 새 직원 유형을 추가할 때 마다 코드를 변경해야하기 때문이다.새 직원 유형을 추가할 때 마다 코드를 변경해야하기 때문이다.
- 해결방법 : 팩토리 패턴으로 추상 팩토리 메서드를 이용하는 방법!
- Ex) 새 직원 유형을 추가할 때 마다 코드를 변경해야하기 때문이다.새 직원 유형을 추가할 때 마다 코드를 변경해야하기 때문이다.
- 열거형 타입인 ENUM을 쓰는 이유 ? 사용자 편의성으로서 타입을 봐주는 것이다! 또한 데이터 타입이 정해지면, 내부코드가 바뀌면서 변수 타입도 전부 다 바꾸어야 하므로!
a. 실습 코드(수정 전) :
- 수정하기
- 각각의 객체들이 적절한 책임과 책임의 범위를 가지고 있는지 확인해보고, 너무 많은 책임과 넓은 범위의 책임을 가지고 있다면 적절하게 수정해보자.
- Barista.java
import java.util.UUID;
public class Barista {
private int rank; // 0: Beginner 1: Middle 2: Master
private int status; // 0: Waiting 1: Making
public Barista(int rank, int status) {
this.rank = rank;
this.status = status;
}
private void setRank(int rank) {
this.rank = rank;
}
private void setStatus(int status) {
this.status = status;
}
public String makeBeverageTo(UUID orderId, Order o) {
o.changeOrderStatus(1);
StringBuilder makedOrders = new StringBuilder();
makedOrders.append("주문ID: ")
.append(orderId.toString())
.append("\n");
o.getOrderDetailInfo().forEach(((beverage, quantity) -> {
makedOrders.append(beverage.getMenuName())
.append(":")
.append(quantity);
}));
o.changeOrderStatus(2);
return makedOrders.toString();
}
}
- Beverage.java
import java.util.Collections;
import java.util.Map;
public class Beverage {
private final String menuName;
private final long price;
private final Map<String, Long> extraRecipe;
public Beverage(String m, long p, Map<String, Long> r) {
this.menuName = m;
this.price = p;
this.extraRecipe = r;
}
public String getMenuName(){
return menuName;
}
public Beverage(String m, long p) {
this(m, p, Collections.emptyMap());
}
public long calculatePrice() {
long extraRecipeTotalAmount = 0L;
if (!extraRecipe.isEmpty()) {
for (String extraMenu : extraRecipe.keySet()) {
extraRecipeTotalAmount += extraRecipe.get(extraMenu);
}
}
return getPrice() + extraRecipeTotalAmount;
}
private long getPrice() {
return this.price;
}
}
- Cashier.java
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
public class Cashier {
private static final Cafe cafe = new Cafe();
private static final OrderBook orderBook = new OrderBook();
public UUID takeOrder(Map<Beverage, Integer> receivedOrders) {
UUID newOrderId = createOrderId();
orderBook.add(newOrderId, new Order(receivedOrders));
return newOrderId;
}
public String sendOrder(Barista toBarista, UUID withOrderId){
return toBarista.makeBeverageTo(withOrderId, orderBook.getOrder(withOrderId));
}
public String completeOrder(UUID u, String message){
orderBook.remove(u);
return message;
}
public long calculateTotalPrice(Map<Beverage, Integer> receivedOrders) {
AtomicLong totalPrice = new AtomicLong(0L);
receivedOrders.forEach(((beverage, quantity) -> {
totalPrice.addAndGet((long) beverage.calculatePrice() * quantity);
}));
return totalPrice.get();
}
private UUID createOrderId(){
return UUID.randomUUID();
}
}
- Cafe.java
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
@Component
public class Cafe {
private final String name;
private Long sales;
public Cafe(){
this.name = "wantedCodingCafe";
this.sales = 10000L;
}
public String getCafeName(){
return name;
}
public void plusSales(Long amount){
this.sales += amount;
}
public void minusSales(Long amount){
this.sales -= amount;
}
}
- Customer.java
import java.util.Map;
public class Customer {
private String paymentMethod;
private final Cashier cashier;
private final Map<Beverage, Integer> myOrders;
public Customer(String p, Map<Beverage, Integer> o, Cashier c) {
this.paymentMethod = p;
this.myOrders = o;
this.cashier = c;
}
private void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}
public String buy() {
long totalPrice = cashier.calculateTotalPrice(myOrders);
return cashier.takeOrder(myOrders, totalPrice);
}
}
- Order.java
import java.util.*;
public class Order {
private final Map<Beverage, Integer> orderGroup;
private int status; // 0: pending 1: processing 2: completed
public Order(Map<Beverage, Integer> o, int s){
this.orderGroup = o;
this.status = s;
}
public Order(Map<Beverage, Integer> o){
this(o, 0);
}
public Map<Beverage, Integer> getOrderDetailInfo(){
return this.orderGroup;
}
public void changeOrderStatus(int orderStatus) {
updateOrder(orderStatus);
}
private void updateOrder(int status) {
this.status = status;
}
}
- OrderBook.java
import java.util.HashMap;
import java.util.UUID;
public class OrderBook {
private static final HashMap<UUID, Order> orderForms = new HashMap<>();
public void add(UUID u, Order o){
orderForms.put(u, o);
}
public void remove(UUID u){
orderForms.remove(u);
}
public Order getOrder(UUID u){
return orderForms.get(u);
}
}
- CafeController.java
import com.cafe.service.CafeService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
@RequestMapping("/cafe")
@RequiredArgsConstructor
public class CafeController {
private final CafeService cafeService;
@GetMapping("hello")
public String welcomeMessage(){
return "Welcome to The Wanted coding cafe!!";
}
@GetMapping("test")
public String test(){
return "test";
}
@GetMapping("order")
public String orderFromMenu(){
HashMap<String, Integer> menu = new HashMap<String, Integer>();
menu.put("AMERICANO", 3);
return cafeService.orderFrom(menu);
}
}
- CafeController.java
import com.cafe.service.handler.Cafe;
import com.cafe.service.handler.Cashier;
import com.cafe.service.handler.Customer;
import com.cafe.service.handler.Beverage;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class CafeService {
private final Cafe wCafe;
public String orderFrom(HashMap<String, Integer> menu){
// 들어 온 주문에 따라서 적절한 `Beverage` 객체를 상속 받은 객체를 생성
// Cashier 생성자 파라미터에 Barista 추가 필요
Cashier cashier = new Cashier(wCafe);
Map<Beverage, Integer> myOrders = new HashMap<>();
myOrders.put(new Americano(), 3);
Customer c1 = new Customer("Card", myOrders);
return c1.buyCoffee(cashier);
}
}
b. 실습 코드(수정 후) :
- Beverage.java
- Cashier.java
- Barista.java
1) 객체 및 캡슐화를 이용하는 과정**
- 객체에게 스스로 책임 부여
- 객체를 캡슐화하기
- 외부접근은 public 인터페이스 이용
- 결론은 객체가 슬롯이나 변경에 유용하도록 설계하기
2) 객체지향적 프로그래밍 설계관점**
- 객체에게 역할이나 책임 부여(객체가 무엇을 하는지)
- 객체부터 설계하고나서 의미가 비슷한 것끼리 묶어 공통화 후 추상화하기 이것은 즉, 클래스가 만들어진다.
- 즉, 클래스 보다는 객체가 먼저다.**
- 객체를 먼저 만들고나서 공통화 작업을 하면 추상화를 할 수 있기 때문에 그 후에 클래스가 만들어지기 때문에 클래스보다는 객체가 먼저다.
- Ex) 캐셔보다는 음료에 정책이 붙는다. 슬롯이 교체되는 것 때문이다.
- 추후 경우에 따라 인터페이스 생성**
3) of() 메서드 만드는 법
- Entity 클래스에서 of() 메소드를 만들어서 DTO로 반환
public class Post extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
public void updateBasicInfo(PostDto dto) {
title = dto.title();
content = dto.content();
}
public PostDto of() {
return new PostDto(id, title, content,null, getCreateDate(), getModifyDate());
}
}
4) 팩토리 메서드 사용법
- switch 문을 이용해 type을 보고 적절한 구현체를 생성해서 런타임 시, 활용하는 방법
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay();
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
@Configure
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch(r.type){
case COMMISSION:
return new ComissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
}
}
}
@Service
public class EmployService{
private EmployeeFactoryImpl employeeFactory;
public Money getTotalAmount(EmployeeRecord r){
Employee e = employeeFactory.makeEmployee(r);
if(e.isPayday()){
return e.calculatePay();
else
return Money.ZERO;
}
}
}
5) SOLID
a. SRP
-
메서드는 하나의 기능을 가져야만 한다.
-
클래스는 하나의 책임을 가져야만한다!
b. OCP
- 구현하는 것보다는 추상 클래스에 주의깊게 보자!
c. LSP
-
자식클래스의 객체를 부모객체에 담아서 사용해도 문제가 없어야 한다.
-
자식은 프로그래밍적 판단도 같이 부모의 것을 상속받아야 한다. 부모의 기능 명세도 같이 받아야 한다!
6) 언제 인터페이스? 언제 추상화를 쓰는지?**
-
a. 추상화가 되어있는 방법에서 상태값을 공유해서 사용하면, 추상화를 사용하고 아니라면 인터페이스를 사용한다!
-
b. 꼭 필요한것만 구현하려면 인터페이스를 사용하자! 필요하지 않는 부분도 구현되어야 한다면 추상화를 사용하자!!
-
c. DB를 붙인다면, 필요한 필드만 엔티티(DTO)로 빼고 필요한 메서드만 인터페이스(클래스로)로 확장시킨다.**
7) 결론
- 하지만, OOP가 항상 옳은 것은 아니다. 절차지향적 프로그래밍과 트레이드오프(복잡성 증가)관계이다. DTO나 Entity는 절차지향적으로 설계되기 때문이다.
8) 추가적으로 공부**
-
이펙티브 자바 3/E
-
토비의 스프링
-
멀티 모듈, 자료구조, 알고리즘
-
네트워크, DB, OS 정리
-
개발 :
- Redis : Refresh Token, Access Token
- Kafka pipe line
- Spring Batch