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을 마무리하고자 한다.