[항해 99] 4주차 WIL(Weekly, I Learned)

gimseonjin616·2022년 7월 17일
0

프로젝트 후기

목록 보기
7/9

주차 : 4주차

기간 : 2022.07.09 ~ 2022.07.15

활동 내용 : Java Web Framework - Spring 심화


Chapter 1. 힘차게 시작하자!!!

지난 주차 과제를 성공리에 마무리한 나... 매우 당당하게 이번 주차를 시작했다.

이번 주차도 매우 매우 성공적으로 보내기 위해서 있는 힘껏 노력해보자!!!
(근데 슬슬 WIL 쓸 때 퀄리티가 떨어지는 것처럼 느껴지는 것은 기분탓인가...)


Chapter 2. 이번 주차 WIL 키워드는 무엇인가?!?

이번 주차 WIL 키워드는 바로 MVC, SQL, ORM이다.

이 세 가지 키워드는 유기적으로 연결되어 있기 때문에 하나씩 차근 차근 정리해나가려고 한다.

참고로 위 세 가지 키워드는 Spring이나 웹에 국한되는 것이 아니라 모든 어플리케이션에서 사용되는 전반적인 내용이다.


Chapter 3. 첫 번째 키워드 MVC!

첫 번째 키워드는 MVC다. MVC는 Model - View - Controller의 약자로 각 역할에 따라 계층을 분리한 대표적인 디자인 패턴이다.

각 계층별로 역할은 다음과 같다.

  • Model : 데이터를 저장하는 객체로 데이터의 속성을 정의하며 Client로 부터 데이터를 받아오거나 DB에 데이터를 저장할 때 사용된다.

  • View : 사용자와 커뮤니케이션을 담당하는 계층으로 사용자의 입력을 받아오고 서버에서 처리한 결과를 사용자에게 보여준다.

  • Controller : 실제 비즈니스 로직을 처리하는 곳이다. View를 통해 받아온 데이터를 처리하여 Model을 통해 데이터베이스에 저장하고 이를 View에 전달한다.

Model 예시

public class UserModel implements ModelInterface {
    private String name;
    ArrayList<Observer> observerList;
    ArrayList<Account> accounts;

    public UserModel(String name){
        this.name = name;
        accounts = new ArrayList();
        observerList = new ArrayList();
    }

    public String getName(){
        return name;
    }

    @Override
    public ArrayList<Account> getInfo() {
        return accounts;
    }

    public void addAccount(int type) {
        if(type == 0)
            accounts.add(new FeeAccount());
        else
            accounts.add(new MinusAccount());
        notifyObservers();
    }

    @Override
    public void registerObserver(Observer o) {
        this.observerList.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        this.observerList.remove(o);
    }

    @Override
    public void notifyObservers() {
        for (Observer o : observerList)
            o.update();
    }
}

View 예시

public class UserView implements ActionListener, Observer {
    UserModel userModel;
    UserController userController;
    JFrame mainFrame;
    JPanel mainPanel;
    JScrollPane jScrollPane;
    DefaultTableModel tableModel;
    JTable table;
    JButton addAccountButton;
    JButton selectButton;
    JDialog accountInputJDialog;

    public UserView(UserModel userModel, UserController userController) {
        this.userController = userController;
        this.userModel = userModel;
        this.userModel.registerObserver(this);
    }

    public void createViews(){
        this.mainFrame = new JFrame();
        this.mainPanel = new JPanel();
        this.tableModel = new DefaultTableModel();
        this.table = new JTable(tableModel);
        this.jScrollPane = new JScrollPane(table);
        this.addAccountButton = new JButton("계좌 계설");
        this.selectButton = new JButton("계좌 선택");
        this.accountInputJDialog = new AccountInputDialog(mainFrame, "Account Input", userController);
        addAccountButton.addActionListener(this);
        selectButton.addActionListener(this);
        tableModel.addColumn("Account Type");
        tableModel.addColumn("Account Balance");
        update();

        mainPanel.add(jScrollPane);
        mainPanel.add(addAccountButton);
        mainPanel.add(selectButton);
        mainFrame.add(mainPanel);
        mainFrame.setVisible(true);
        mainFrame.setSize(540, 540);
        mainFrame.setResizable(false);
        mainFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    }


    @Override
    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == addAccountButton) {
            accountInputJDialog.setVisible(true);
        } else if(e.getSource() == selectButton){

            int rowIndex = table.getSelectedRow();

            Account account = userModel.getInfo().get(rowIndex);
            account.registerObserver(this);
            AccountController accountController = new AccountController(account,mainFrame);
        }
    }

    @Override
    public void update() {
        tableModel.setRowCount(0);
        if(userModel.getInfo() != null)
            for (Account account : userModel.getInfo())
                this.tableModel.addRow(new Object[] {account.getName(), account.getBalance()});
    }
}

class AccountInputDialog extends JDialog{
    JComboBox jComboBox = new JComboBox(new DefaultComboBoxModel(new String[]{"Fee Account","Minus Account"}));
    JButton okButton = new JButton("확인");

    public AccountInputDialog(JFrame jFrame, String title, ControllerInterface userController){
        super(jFrame,title);

        setLayout(new FlowLayout());

        add(jComboBox);
        add(okButton);

        setSize(200,100);

        okButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                userController.add(jComboBox.getSelectedIndex());
                setVisible(false);
            }
        });

    }
}

Controller 예시

public class UserController implements ControllerInterface {

    UserModel userModel;
    UserView userView;

    public UserController(UserModel userModel){
        this.userModel = userModel;
        this.userView = new UserView(userModel,this);
        userView.createViews();
    }

    @Override
    public void add(String user) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void add(int type) {
        this.userModel.addAccount(type);
    }

}

Chapter 4. 두 번째 키워드 SQL!

이 MVC 패턴을 통해 역할이 나뉘면서 각 계층에 적합한 언어들을 사용할 수 있게 됐다.

View는 HTML, CSS, JAVASCRIPT나 Swing, Pyqt5 등이 있으며 Controller의 경우, Java, Python, Node js 등이 있다.

그리고 Model에는 SQL이 있다.

SQL은 Structure Query Language의 줄임말로 구조화 질의문이라고 해석할 수 있다.

SQL은 연관되어 있는 데이터 끼리 묶고 묶은 데이터들 사이의 관계를 정의하여 데이터 중복을 줄이고 데이터 무결성을 충족시키는 언어다.

좀 더 쉽게 얘기하면 SQL은 데이터를 표로 저장하고 필요에 따라 테이블을 연결하여(관계) 데이터를 표현하는 것이다.

SQL은 데이터 CRUD에 해당하는 INSERT, SELECT, UPDATE, DELETE가 있으며 관계 매핑을 위한 JOIN, 검색을 위한 WHERE, 정렬 및 통계를 위한 ORDER BY, GROUP BY, COUNT 등이 있다.

SQL 예시는 정말 많기 때문에 INSERT, SELECT, UPDATE, DELETE만 정리하겠다.
(나 다 알고 있음 진짜임!!)

INSERT 예시

insert into subject (title, desciption) values("수학", "제일 싫어하는 함수");

SELECT 예시

select title from subject where title = '수학';

UPDATE 예시

update subject set description = "이제는 좀 좋아진 과목" where title = "수학";

DELETE 예시

delete from subject where title = "수학";

Chapter 5. 세 번째 키워드 ORM!

마지막 ORM이다. ORM 은 Object Relational Mapping의 줄임말로 SQL과 Model 간의 자동 변환기 같은 것이다.

이게 무슨 말인고 하니, SQL은 표로 데이터를 저장한다. 하지만 우리가 사용하는 Programing 언어에서는 객체, 즉 class로 데이터를 다룬다.

예를 들어보자, SQL은 User 데이터를 아래와 같이 저장한다.

이름나이성별
김선진27

반면 Java에서는 아래와 같이 저장한다.

class User{
	private String name;
    private int age;
    private String sex;
    
    public User(String name, int age, String sex){
    	this.name = name;
        thish.age = age;
        this.sex = sex;
    }
 }
 
 public static void main(String args[]){
 	User user = new User("김선진", 27, "남");
 }

즉 Java(그 외 모든 언어)에서 SQL로 가져온 데이터를 표에서 객체로 변환시켜줘야한다.

이것을 자동으로 해주는 것이 바로 ORM이다. ORM 이전까지는 DBMS를 통해서 데이터베이스에서 데이터를 바이트 단위로 읽어와서 키 밸류를 통해 객체에 저장해줬다.

stmt = con.createStatement();
//데이터를 가져온다.
rs = stmt.executeQuery("select name, age, sex from user");
 
while(rs.next()){
	//출력
    User user = new User(
	    rs.getString("name"),
	    rs.getString("age"),
    	rs.getString("sex")
    )
}

지금 가져온 예시는 단순히 SQL과 Model을 매핑하는 부분만 가져왔고 그 외 DB 접근과 Connection 관리, Driver 관리 등 많은 코드가 필요로 한다.

따라서 SQL과 Model 매핑은 반복적인 작업이 많고. 비즈니스 로직과 SQL에 모두 심혈을 기울여야 했다. 이에 ORM을 만들어서 model 매핑을 자동으로 하여 개발자가 비즈니스 로직에 집중할 수 있도록 만들어 줬다.


Chapter 6. 보너스 키워드 N+1 문제

N+1 문제가 무엇이냐, 정의는 다음과 같다.

N-1 관계 모델을 조회할 때, JOIN으로 데이터를 가져오는 것이 아니라 모든 엔티티를 SELETE Query로 가져오기 때문에 총 N+1 개의 Query가 발생한다.

즉 다대일 관계 객체를 조회할 때, N+1개의 쿼리가 발생한다는 것이다.

쿼리가 많이 생기는 것이 뭐가 문제냐? 할 수 있다.

그러나 여기서 중요한 것은 DB는 커넥션 pool이라고 해서 DB에 한번에 접근할 수 있는 Quary 수를 제한한다.

제한하는 이유는 커넥션 생성(커넥션 뿐만 아니라 모든 자원의 생성)은 많은 비용을 요구한다. 많이 만들면 서버가 힘들어한다!

다시 본론으로 돌아와서 커넥션은 갯수가 제한되어 있다. 그리고 Query 하나 당 한 개의 커넥션이 필요하다. 따라서 Query가 많이 발생되면, 그만큼 커넥션이 부족해져서 서버에 문제가 생긴다.

예를 들어 팀 조회 로직에서 N+1 문제로 Query 가 100개가 생겼다. 그에 반해 커넥션 수는 10개가 있다. 그러면 먼저 10개가 실행되고 커넥션 수가 남을 때 까지 남은 90개의 Query는 대기하게 된다.

이때 팀 조회 로직 말고 회원 조회나 다른 로직에서 query를 보내면 앞에 쌓여있는 90개의 query가 다 처리 될 때 까지 기다려야 한다. 즉 웨이팅이 엄청나게 많아진다!!!
(이를 병목이라고 한다.)

이처럼 병목이 많아지면 전체적인 서비스 성능이 떨어지고 유실되는 데이터가 생길 수 있다.

이를 해결할 수 있는 대표적인 방법으로 2가지가 있다.
(몇 개 더 있는데 내가 알고 있는 방법은 2가지다.)

첫 번째로 LAZY 로딩을 활용하는 것이다.

LAZY 로딩은 말 그대로 나중으로 미룬다는 뜻으로 Proxy 패턴을 활용하여 나중에 필요할 때, 데이터를 가져온다는 것이다.

데이터는 필요할 때 가져오는 것이 맞는데 무슨말인가????

기본적으로 데이터베이스에서 데이터를 가져올 때, Model 단위로 가져온다. 즉 Team 객체의 teamName이 필요해서 Team 객체를 가져왔는데 뜻밖에 팀원 List도 가져와서 N+1이 발생한다는 것이다.

class Team(){
	private String teamName;
    @OneToMany
    private List<User> teamMembers;
}

// 이 로직에서는 teamMembers가 필요없지만 데이터베이스에서 가져오기 때문에 N+1 문제 발생!!!
Team team = teamRepository.findById(1L);
System.out.println(team.getTeamName());

이때 Lazy 로딩을 사용하면 실제 teamMembers에 접근할 때 데이터베이스에서 데이터를 가져온다.

Lazy 로딩 설정은 관계 매핑 어노테이션 설정에 fetch 조건을 주면 된다.

class Team(){
	private String teamName;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<User> teamMembers;
}

// 이 때는 teamMembers 가져오지 않음 -> N+1 문제 발생 x
Team team = teamRepository.findById(1L);
System.out.println(team.getTeamName());


// 이 때 teamMembers 가져옴!!
System.out.println(team.getTeamMembers());

두 번째 방법은 QueryDSL을 사용하는 것이다.

QueryDSL은 말 그대로 SQL 구문을 직접 작성하여 Object Mapping을 하는 것으로 복잡한 데이터 관계를 매핑할 때, 주로 사용된다.

이 QueryDSL은 N+1을 어떻게 해결하냐면 직접 JOIN query문을 주입하여 한꺼번에 데이터를 가져온다.


// QueryDSL은 아직 서툴고 임의로 작성한거라 작동 안하거나 문법이 틀렸을 수 있습니다.
@Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
    @Query(value = "select DISTINCT c from Team c left join fetch c.teamMembers")
    List<Team> findAllWithMembers();
}

위 방식으로 한번의 쿼리로 모든 TeamMembers를 가져올 수 있다. 하지만 이 방식으로 하면 초기 선언에 teamMembers를 가져오고 만약 teamMembers를 사용하지 않는 로직이라면 메모리 낭비만을 초래할 수 있다.

결론 - 다대일 관계에서 데이터 조회가 많지 않은 경우에는 Lazy loading을 사용하여 필요한 경우에만 가져오고, 많은 경우에는 QueryDSL을 사용하여 한 번에 가져오자!

물론 위 두 개말고 더 많은 N+1 문제 해결 방법이 있다. 찾아보니 Spring data JPA에서 제공하는 Graph 방식 등이 있다고 한다.

나중에 조금 더 공부해봐야겠다.
(음 마무리하기가 힘들군! 마무리다 끝! 어쨌든 끝!!!)


Chapter 7. 4주차 끝!!

이번 주차도 성공적으로 잘 마무리 한 거 같다.

개인 과제로 Spring Security와 테스트 코드 작성이 나왔는데 Security에서 정말 많이 애를 먹었다.

얘는 한 3~4번 쓴 거 같은데도 매번 나를 고생시킨다. 특히 2.7.0부터는 Sprig Security 설정 방식도 바껴서 새로 공부한다고 더욱 고생했던 거 같다.

이번에 공부한 Security 내용은 나중에 블로그에 한번 더 정리해볼 것이다.

마지막으로 기술 매니저님께 받은 칭찬을 자랑하면서 이번 WIL을 마무리하고자 한다.

profile
to be data engineer

0개의 댓글