<토이 프로젝트 회고록> 자바 스윙(Java Swing)을 이용한 GUI 프로그램(ft. DBeaver)

Posted by Space_Jin
2021. 12. 29. 20:21 Programming/Java, Kotlin
728x90
반응형

 

Java내에 내장되어있는 GUI인 스윙(swing)을 이용하여 간단한 토이 프로그램(가계부 프로젝트)을 작성하였습니다.

 

어떤 기능을 어떻게 만들었는지 생각해야할 부분이 어떤 게 있을지 회고하기 위해 기록합니다.

 

》》》바로가기

  1. 로그인
  2. 회원가입
  3. 텍스트 필드 힌트 삽입
  4. 테이블 데이터 삽입
  5. 테이블 새로고침
  6. 테이블 행 삭제

▶로그인 기능

ID 혹은 PW 미 기입 시

아이디 혹은 비밀번호를 기입하는 텍스트필드(textfield)가 기입되지 않으면 하단 라벨에 경고문을 생성.

올바르지 않은 ID 혹은 PW 입력 시

옳지 않은 아이디나 비밀번호를 입력했을 때, 경고문 GUI 생성.

 

로그인 버튼에 이벤트를 처리해주는 메서드 actionListener 객체를 생성해 더 해주고 텍스트가 비어있을 때, 정확히는 idHint, pwHint 문자열과 동일한 경우 라벨 경고문이 보일 수 있게 해 줬다.

추가로 유저 정보가 담겨있는 DB와 일치하지 않은 아이디나 비밀번호를 입력한다면 경고창을 띄어줬다.

 

※ 새로운 GUI 경고창을 띄우는 것보다는 하단 라벨의 문자열을 바꿔주는 것이 성능적 이점이 있지 않을까 생각한다.

ms로 아주 짧은 시간이지만 성능의 차이는 확실하다. (실제 속도 차이 약 20~30배)

 

▷ 로그인 버튼(jButton1)에 "login"이라는 이름의 actionListener 객체를 추가

		jButton1.addActionListener(login); // login 이벤트 리스너

 

▷ "login" actionListener 객체 정의

	// "로그인" 버튼 클릭 이벤트
	private ActionListener login = new ActionListener() {
		@Override
		public void actionPerformed(ActionEvent e) {
			String id = textField1.getText();
			String pw = textField2.getText();
			if (textField1.getText().equals(idHint) || textField2.getText().equals(pwHint)) {
				jLabel4.setVisible(true);
			} else {
				if (bd.checkIdPw(id, pw)) {
					System.out.println("아이디 확인");
					jLabel1.setVisible(false);
					new SelectAllGui(id);
					dispose();
				} else {
					System.out.println("로그인 오류");
					jLabel4.setVisible(true);
					JOptionPane.showMessageDialog(null, "아이디 또는 비밀번호를 확인하세요!");
				}
			}
		}
	}; // end login trigger

 

1. 로그인 버튼이 눌리는 순간 id와 pw 텍스트 필드를 가져온다.

2. 각 텍스트 필드의 값이 idHint(텍스트 필드에 기본으로 쓰여있는 문구) 혹은 pwHint와 일치한다면 숨겨 놓았던 하단 문자열 라벨(jLabel4)을 보여준다(". setVisible(true)").

3. 두 텍스트 필드에 값이 있다면 미리 만들어두었던 id와 pw를 DB와 확인하는 메서드를 실행한다.

-> 일치하는 데이터가 존재할 경우 : id를 인자로 필요한 데이터를 가져온다.

-> 일치하는 데이터가 존재하지 않는다면, 경고창을 띄어준다.(". showMessageDialog")

▶회원가입 기능

회원가입 기능 흐름

1. 로그인과 동일하게 하나라도 비어있는 텍스트 필드가 존재(힌트와 동일한 문자열)할 경우 하단에 경고문구를 출력.

2. DB에서 정의한 필드의 크기보다 더 큰 데이터가 들어올 수 없게 텍스트 필드의 문자열을 제한.

3. "초기화" 버튼 클릭 시 텍스트 필드를 모두 "힌트"와 동일하게 변경.

 

▷ "회원가입" GUI에 포함된 "등록 버튼"과 "초기화 버튼" 이벤트

	// "등록" 이벤트
	private ActionListener userJoin = new ActionListener() {
		@Override
		public void actionPerformed(ActionEvent e) {
			
			if (jTextField1.getText().equals(idHint) || jTextField2.getText().equals(pwHint)
					|| jTextField3.getText().equals(phHint) || jTextField4.getText().equals(nameHint)) {
				jLabel6.setText("모든 항목을 입력해주세요");
			} else {
				if (jTextField1.getText().length() > 10) {
					jLabel6.setText("아이디가 너무 깁니다.\n(영문, 숫자 조합 10자 이내)");
				} else if (jTextField2.getText().length() > 8) {
					jLabel6.setText("비밀번호가 너무 깁니다.\n(영문, 숫자 조합 8자 이내)");
				} else if (jTextField3.getText().length() > 11) {
					jLabel6.setText("전화번호가 너무 깁니다.\n(숫자 11자 이내)");
				} else if (jTextField4.getText().length() > 8) {
					jLabel6.setText("이름이 너무 깁니다.\n(한글 8자 이내)");
				} else {
					vo = new UserinfoVo(jTextField1.getText(), jTextField2.getText(), jTextField3.getText(),
							jTextField4.getText());
					bd = UserinfoDao.getInstance();
					
					if (!bd.idcheck(vo.getId())) {
						bd.insert(vo);
						dispose();
					} else {
						JOptionPane.showMessageDialog(null, "이미 존재하는 아이디입니다");
					}
				}
			}

		}
	}; // userJoin end

	// "초기화" 버튼 이벤트
	private ActionListener initTextField = new ActionListener() {
		@Override
		public void actionPerformed(ActionEvent e) {
			jTextField1.setText(idHint);
			jTextField2.setText(pwHint);
			jTextField3.setText(phHint);
			jTextField4.setText(nameHint);
		}
	}; // init end

▶텍스트 필드 힌트 삽입

텍스트 필드 힌트란? 텍스트 필드 속에 워터마크와 같이 실제 기입된 값은 아니지만(null 역할) 사용자가 확인할 수 있는 텍스트입니다. ex) "아이디를 입력해주세요."

힌트가 삽입된 텍스트 필드

텍스트 힌트는 실제는 존재하지 않는 값이 되어야 하기에 아래와 같은 기능을 수행해야 합니다.

1. 힌트와 동일한 문자열은 null로 취급.

2. 텍스트 필드에 값을 기입하려 할 때, 힌트 값이라면 비어있는 텍스트 필드(공백)가 되어야 함. 

3. 텍스트 필드에 값을 기입하던 중이라면, 그대로 수정이 가능해야 함.

4. (옵션) 사용자 기입 값과 힌트를 구별하기 위해서 다른 폰트와 텍스트 색상을 사용함.

텍스트 필드 힌트의 기능

		Font gainFont = new Font("Tahoma", Font.PLAIN, 11);
		Font lostFont = new Font("Tahoma", Font.ITALIC, 11);

		textField1.setText(idHint);	// 텍스트 필드 힌트의 기본 문자
		textField1.setFont(lostFont);	// 텍스트 필드 힌트의 기본 폰트
		textField1.setForeground(Color.GRAY);	// 텍스트 필드 힌트의 기본 색상
		textField1.addFocusListener(new FocusListener() {	// 텍스트 필드 포커스 시 이벤트

			@Override
			public void focusLost(FocusEvent e) {	// 포커스를 잃었을 때,
				if (textField1.getText().equals("")) {
					textField1.setText(idHint);
					textField1.setFont(lostFont);
					textField1.setForeground(Color.GRAY);
				}
			}

			@Override
			public void focusGained(FocusEvent e) {	// 포커스를 얻었을 때,
				if (textField1.getText().equals(idHint)) {
					textField1.setText("");
					textField1.setFont(gainFont);
					textField1.setForeground(Color.BLACK);
				}
			}
		});

이때, 텍스트 필드의 폰트가 변경되게 되는 게 인코딩과 관련된 문제가 발생할 수 있다.

 

텍스트 필드에 삽입된 데이터 중 한글을 가져와야 한다면, 한글을 지원하는 폰트를 사용해야 오류 발생이 없다. (한글을 직접 기입할 때, 깨질 수 있으니 확인해야 한다.)

나의 경우, 한글 데이터를 가져올 때, "Glum12" 폰트를 사용했다.

▶테이블 데이터 삽입

private DefaultTableModel model;
private JTable jTable1;
private JScrollPane jScrollPane1;
List<String[]> rows;
String[][] data;
String[] columns;

// ArrayList인 rows를 2차원 배열 data로 변환 후 매핑
rows = dao.getListById(id); // 데이터 튜플(ArrayList)
columns = new String[] { "수입|지출", "날짜", "금액", "분류", "메모", "계좌번호" , "IDX"}; // 컬럼
data = rows.stream().toArray(String[][]::new); // ArrayList -> Array

// jTable1 생성
model = new DefaultTableModel(data, columns); // 테이블에 붙일 모델 객체 생성(데이터, 컬럼)
jTable1 = new javax.swing.JTable(model);	// 테이블 객체 생성
jScrollPane1.setViewportView(jTable1);	// 패널에 테이블 붙이기
		
// jTable1 컬럼 별 정렬 기능 추가
TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(jTable1.getModel());
jTable1.setRowSorter(sorter);

스윙에서 테이블을 보여주기 위해선 3가지 단계를 거친다.

1. 테이블에 삽일할 행(튜플)과 열(칼럼)을 인자로 받은 모델(model) 객체 생성

2. 생성한 모델을 인자로 받아 테이블(JTable) 객체 생성

3. 생성한 객체를 패널에 삽입

 

여기서 모델(mode) 객체의 인자로 들어가는 행 데이터는 배열(Array) 객체여야 하는데 나의 경우 쿼리로 데이터를 ArrayList로 가져오기 때문에 한번 더 Array로 변환(ArrayList -> Array)해야 하는 불합리가 있었다.(ArrayList 인 "rows"를 Array인 "data"로 캐스팅(casting) 필요)

=> 추가적으로 수정을 고려해야 할 사항 (성능적 측면)

▶테이블 새로고침

// "새로고침" 이벤트
private void jButton2ActionPerformed(ActionEvent evt, String id) {
	// query 데이터를 가져오기 위한 DB와 연결된 객체
	InexDao dao = InexDao.getInstance();
	
    // 새 list를 가져오기
	rows = dao.getListById(id);
	
    // columns = new String[] { "수입|지출", "날짜", "금액", "분류", "메모", "계좌번호" , "IDX"};
	data = rows.stream().toArray(String[][]::new);
	
    // 새 모델 생성 후 테이블 생성
	model = new DefaultTableModel(data, columns);
	jTable1 = new JTable(model);
	
	// jTable1 sort
	TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(jTable1.getModel());
	jTable1.setRowSorter(sorter);
    
    jScrollPane1.setViewportView(jTable1);
}

"새로고침" 버튼 이벤트의 경우 변경된 테이블의 리스트를 가져와 새로운 모델(model) 객체를 생성한 후 또다시 테이블(JTable)을 생성하는 것이다.

 

cloumn의 경우 전역 변수로 생성했다면 다시 생성할 필요는 없다.

 

칼럼 별 정렬을 담당하는 "TableRowSorter" 역시 하나의 객체이기 때문에 모델 객체와 함께 다시 생성해준다.

 

새로 생성한 테이블을 기존에 있던 패널에 다시 세팅해줌으로써 새로고침 기능으로 사용할 수 있다.

▶테이블 행 삭제

테이블의 행 클릭 후 삭제

▷삭제 기능

1. 삭제를 원하는 튜플을 선택한다.

2. 삭제 버튼 클릭 -> 테이블에서 삭제된 후 DB에 적용.

	// "삭제" 이벤트
	private void jButton3ActionPerformed(ActionEvent evt) {
		int row = jTable1.getSelectedRow();	// 선택된 행 번호
		int idx = Integer.valueOf((String)jTable1.getValueAt(row, 6));	// 선택된 행, 열의 데이터 값
		
        InexDao dao = new InexDao();	// DB 조작을 위한 객체 생성
		
		model.removeRow(row);	// 모델의 행 삭제
		dao.delete(idx);	// DB삭제 메서드
		
		System.out.println(row +"  "+ idx);	// 행, 삭제 조건 데이터 값 출력(확인용)
	}

현재 모델(model)에서 행을 삭제하는 내장 메서드(removeRow(삭제할 행))을 사용하면 GUI 테이블에 바로 적용된다.

(만약, 필터 기능을 이용해서 칼럼 별 필터를 사용하고 있다면, 바로 적용되지 않는다.)

 

getSelectedRow() 함수를 이용하면 jTable에서 선택된 행을 반환한다. 만약, 테이블이 setEnable(false)로 설정되어있다면 행이 선택되지 않으므로 setEnable(true)로 변경해 준다.

 

remoeRow은 DB에는 적용되는 것이 아니기에 미리 생성해 놓은 DB삭제를 위한 메서드를 실행한다.

728x90
반응형

자바[Java] 입출력 Stream 이해하기

Posted by Space_Jin
2021. 12. 29. 20:14 Programming/Java, Kotlin
728x90
반응형

▶InputStream / Sytem.in.read()

ImputStream은 사용자의 입력을 받을 수 있는 역할을 한다.

 

사용자 입력에 자주 쓰이는 클래스인 Scanner와 다른 동작을 보이는데, 가장 큰 특징은 1byte 단위로 입력을 받아온다는 점이다.

 

Sytem.in.read()은 사용자 입력을 임시 저장하는 버퍼(Buffer)에서 저장된 값을 하나씩 가져온다.

 

이 read()의 return 값은 byte단위의 데이터 혹은 -1(버퍼에 값이 없을 때)이다.

▶아스키코드 값으로 저장

read() 메서드로 리턴한 값을 변수에 저장하게 될 텐데, 이때 사용자 입력 값을 리턴 값인 byte단위(0~255)에 맞추어 아스키코드 값을 저장하게 된다.

변수 a에 사용자 입력 a를 저장

변수 a에 사용자 입력으로 문자 'a'를 받으면 문자 'a'의 아스키코드 값이 97이 a 변수에 저장된다.

버퍼의 동작 (\r, \n)

사용자 입력으로 문자 'a'를 받았다고 해서 버퍼에 'a'만 저장되는 것은 아니다.

 

사용자 입력을 완료했을 때, enter 키를 누르게 되는데 버퍼에는 이 enter키가 가지는 복귀 문자(Carriage return, ASCII 13)와 개행 문자(Line feed, ASCII 10)가 함께 저장된다.

 

↓ a 입력 시에 버퍼의 상태

문자 a \r \n
ASCII 97 13 10

여기서 int 변수에는 하나의 정수 값만 저장할 수 있는데 사용자 입력으로 인해 버퍼에 3개의 정수 값이 담긴다.

그러므로 사용자 입력으로 받은 'a'문자를 저장하기 위해서는 3개의 int 변수가 필요한 셈이다.

PrintStream / 한글 입력과 출력

read() 메서드로 1byte씩 저장된 변수를 출력하는 방법은 System.in.write() 메서드를 사용하면 된다.

System.in.write()는 변수에 저장된 1byte의 값을 읽어낸다.

 

read() 함수에서 'a'라는 문자는 각각 97 / 13 / 10 세 개의 값의 합으로 이뤄져 있음을 알 수 있다.

 

반대로 write() 함수로 'a'라는 문자를 출력하기 위해서는 97 / 13 / 10 세 개의 값이 모여야 한다.

코드
출력

여기서 사용자 입력이 한글이면 문제가 생기는데, 한 중 일어의 유니코드는 자바에서는 3byte이기 때문이다.

사용자 입력 : 'ㄱ'
출력

사용자 입력을 'ㄱ'으로 받으면 총 5개의 변수에 저장이 되는데 앞서 말한 한글이 3byte으로 이뤄져 있기 때문이다.

\r과 \n 값을 제외하면 한글 'ㄱ'은 227 / 132 / 177의 합으로 이뤄져 있는 문자이다.

 

위와 같이 다섯 개의 변수를 write() 함수를 이용해서 출력하면 입력 값 'ㄱ'을 제대로 출력할 수 있다.

사용자 입력 : 'ㄱ'

 

출력

while 문 사용

while문 예시

보통은 while문을 사용해서 사용자 입력을 받고 값을 출력해준다.

 

사용자 입력을 받을 때마다 \r, \n이 함께 저장되므로 늘 2byte가 추가로 저장된다.

 

위처럼 while문을 이요해 InputStream / OutStream을 사용할 때는 잘못된 사용자 입력으로 무한 루프에 빠질 위험이 높기 때문에 try / catch 문을 이용해서 예외처리를 해준다. (IDE에서 자동으로 생성해준다.)

 


잘못된 부분 지적은 늘 감사합니다.

728x90
반응형

자바[Java] 인터페이스[Interface] 사용과 이유

Posted by Space_Jin
2021. 12. 29. 20:13 Programming/Java, Kotlin
728x90
반응형

▶ 인터페이스(Interface) 란?

자바에서 제공하는 인터페이스란, 추상 클래스와 마찬가지로 추상된 틀을 만들어 개발에 혼선이 없게끔 도와주는 역할을 하는 객체이다.

 

협업을 하는 개발자들끼리의 원활한 의사소통을 위해서 생성된 개념으로 인터페이스라는 이름을 가지는 것 같다.

 추상 클래스와 무엇이 다를까?

추상 클래스는 트리 형식으로 상위 클래스와 하위 클래스로 나뉘어지는 트리 구조이다.

 

하위 클래스는 상위 클래스에서 추상화시킨 생성자 혹은 메서드를 정의하면서 더 구체화되어간다.

트리 구조

반면에, 인터페이스는 트리 구조와 같은 수직적 구조가 아닌 수평적 구조를 가지게 된다.

수평 구조

수직적 구조를 갖는 추상 클래스는 하나의 상위 클래스에게만 상속받을 수 있지만, 수평적 구조의 인터페이스는 하나의 클래스가 여러 인터페이스를 가져올 수 있다.

2개의 인터페이스 가져오기

 인터페이스는 왜 사용할까?

인터페이스를 배우면서 가장 의문인 부분이었다. 상속과 추상 메서드가 있기 때문에 인터페이스의 존재는 오히려 개발의 복잡성과 높이는 것이 아닌가 생각이 있었다.

나 말고도 많은 사람들이 인터페이스에 대한 논쟁이 넷 상에서 많이 일어나고 있음을 확인할 수 있었다.

 

하지만, 분명 필요에 의해서 자바에서 이 같은 기능을 제공할 것이라 생각 역시 컸기에 여러 가지 의견들을 봤었다.

 

그러던 중 "생활코딩" 채널을 운영하시는 "이고잉"님의 짧은 설명 영상을 보고 바로 이해가 됐다.

 

내가 이해한 내용은 아래와 같다.

 

개발자들이 서로 협업하여 프로젝트를 만들 동안 서로 만들어낼 기능들은 유기적으로 상관관계를 만들어낼 것이다.

 

A개발자가 만들고 있는 A클래스는 B개발자가 만들고 있는 B클래스가 완성이 되어야지 개발이 가능한 경우가 있을 것이다.

A클래스 개발 중 B클래스의 필요성

이럴 경우, A개발자는 B클래스의 더미(dummy) 클래스라는 가짜 혹은 임시 클래스를 만들어서 B클래스가 필요한 자리에 채워 넣은 뒤, B개발자가 B클래스를 완성한 후에 바꿔뀜으로서 개발 일정에 차질이 생기지 않을 수 있을 것이다.

 B더미 클래스와 B클래스의 인터페이스C 활용

B클래스에는 여러 기능을 하는 메서드들이 존재할 텐데 이 메서드들의 형식(틀)을 담당하는 것이 인터페이스가 되겠다.

 

B더미 클래스와 B클래스는 같은 인터페이스 C를 사용하고 있고 인터페이스의 메서드 1과 메소드2는 추상 메소드로서 형식만 갖춰져있다. B개발자가 B클래스에서 메소드1과 메서드 2를 정의하고 B클래스를 완성한다면, A개발자가 B더미 클래스를 B클래스로 교체해주기만 하면 된다.

 인터페이스를 사용하지 않는다면?

인터페이스로 메서드 1과 메서드 2의 형식을 잡아놓지 않는다면, 개발자들 간의 B클래스를 담당하는 B개발자가 메서드 1을 A개발자가 가지고 있는 B더미 클래스 안의메서드 1의 형식이 맞지 않아서 많은 양을 리팩터링 해야 하는 경우가 발생할 수 있다.

메소드 형식의 불일치 -> 수정 필요

 결론

인터페이스는 메서드의 틀을 미리 만들어 개발자 간의 의사소통 혼선을 줄여주고 다형성 개발에 유리함을 가져다주는 객체이다.

728x90
반응형

자바[Java] 접근 제어자[Access modifier]

Posted by Space_Jin
2021. 12. 29. 20:10 Programming/Java, Kotlin
728x90
반응형

자바의 접근 제어자 default / public(package) / private / protected / static / final

▶ default(package)

가장 기본적인 접근 제어자로 생략이 가능하다. => 접근 제어자를 생략하면 default로 설정된다.

default는 같은 패키지 내에 있는 class라면 접근이 가능하다.

testPackage1 패키지 / TestClass1 클래스
testPackage2 패키지 / TestClass2 클래스

1번 패키지에 1번 클래스를 정의하였다.

2번 패키지를 만들고 2번 클래스를 만들고 2번 클래스에 1번 클래스를 불러오기(import)를 했을 때, 오류 발생

=> 같은 패키지 안에서만 불러오기 가능

 

▶ public

이름처럼 어디서든 불러올 수 있다.

 

다른 패키지 안의 다른 클래스에서도 불러와 사용 가능하다.

testPackage1 / TestClass1

testPackage1 패키지에 있는 TestClass2 클래스의  접근 제어자를 public으로 변경해줬다.

testPackage2 패키지 / TestClass2 클래스

testPackage2 패키지의 TestClass2 클래스에서 다른 패키지에 있는 TestClass1을 불러와도 잘 작동된다.

▶ private

자신을 제외한 다른 클래스에서 접근 불가능

 

오직 자신의 클래스 안에서만 접근이 가능하다.

 

private / public 으로 필드 값 생성
public 으로 정의된 publicNum만 접근 가능

testPackage1 패키지의 TestClass1 클래스에 필드 값을 생성했다. (public int, private int 1개씩)

TestClass1은 public이기 때문에 다른 패키지(testPackage2)에서 다른 클래스(testClass2)로 import 할 수 있었지만, 필드 값은 publicNum 밖에 접근하지 못한다.

 

 

▷ private 접근자를 쓰는 이유?

 

클래스는 추상적인 개념을 정리해 놓고 해당 클래스를 상속받은 하위 클래스에서 구체화시켜서 사용한다. ex). 상위 클래스 : 도형, 동물... / 하위 클래스 : 사각형, 삼각형...    강아지, 고양이...

 

이때, 가장 큰 틀인 상위 클래스의 필드 값을 직접 접근하면 하위 클래스들에게 문제가 발생할 수도 있고 사용자의 잘못된 값을 입력받을 수도 있다.

 

ex): 잘못된 값 대입 : 1~100까지만 저장해야 하는 변수에 사용자가 200을 저장 

 

이 같은 문제를 방지하고자 일반적으로 필드 값은 private로 정의하고 getter / setter 함수를 만들어 필드 값에 접근한다.

▶ protected

같은 패키지 혹은 상속한 하위(자식, 서브) 클래스에서만 접근 가능 / 상속받지 않은 클래스는 다른 패키지에서 사용 불가능

protected / private 접근 제어자 필드 값 생성
하위 클래스에서 접근 가능한 필드 값 확인 

testPackage1 패키지 안의 상위 클래스인 SuperClass에서 protected와 private 필드 값을 각각 생성했을 때,

testPackage2 패키지에서 하위 클래스인 SubClass에서는 protected로 정의된 필드 값만 접근이 가능한 걸 확인할 수 있다.

▶ static

▷공유 메모리에 존재

 

static으로 정의된 객체는 프로그램이 실행되는 동안 공유 메모리에 상주하면서 사용된다.

 

일반적으로 객체를 생성할 때면 메모리 공간을 차지하게 된다.

 

하지만, 상수 값처럼 변함없는 값의 경우 반복해서 생성하게 되면 메모리를 낭비하는 꼴이 될 수 있다.

 

이때 반복적으로 사용되는 객체공유 메모리에 한 번만 저장시킨 후 필요할 때마다 불러와 사용하면 메모리를 더 효율적으로 사용할 수 있게 된다. 

 

ex)

A 클래스에는 데이터1, 데이터 2, 데이터 3 이 있다고 가정

 

데이터 1은 값이 절대 변하지 않는다고 했을 때, A클래스를 반복해서 생성하게 불러오게 되면 변하지 않을 값인 데이터 1을 반복해서 메모리에 저장하게 되어 비효율적이다.

 

변하지 않을 데이터 값(예를 들어 상수) 은 static으로 정의하여 공유 메모리에 저장 -> 메모리 효율성 증가

 

 

 

 

 

 

 

 

▷static끼리만 호환

 

static으로 정의된 데이터를 어디서든 사용할 수 있는 것은 아니다.

 

static으로 정의된 데이터는 같이 static으로 정의된 클래스에서만 사용할 수 있다.

 

대표적인 static 클래스는 프로그램을 실행시키는 main 클래스가 있다.

static 데이터와 static 클래스

static으로 정의된 main 클래스에서 static으로 정의된 MAX_NUM 데이터 값이 잘 사용된다.

static 데이터와 static이 아닌 클래스

static이 아닌 클래스 안에서 static으로 정의된 데이터를 불러와 사용할 때의 모습이다.

 

프로그램 시작 전에는 문법적 에러가 없어서 경고문이 나타나지 않지만, 프로그램을 실행하면 위처럼 에러가 나며 실행되지 않는다.

 

MAX_NUM은 공유 메모리에 상주되는 static 데이터인데 static이 아닌 클래스에서 불러왔기 때문이다.

 

이처럼 static 데이터는 static 클래스에서만 사용이 가능하다는 특징이 있다.

 

▶ final

상수를 저장하는 접근 제어자

 

final의 경우는 static에서 잠깐 나왔는데 상수(변하지 않는 값)를 저장할 때 사용된다.

상수 값 변경 오류

final로 정의된 MAX_NUM의 값을 변경하려고 하면 오류 메시지가 나온다.

 

변경되어서는 안 되는 값이 경우, 사용자 혹은 개발자가 실수로라도 변경할 수 없도로 final로 정의한다.

 

▷ 하위 클래스에서 새로 생성 가능

 

final로 정의된 값은 하위 클래스에서 새로 생성 가능하다. -> 같은 이름, 다른 메모리 주소

 

하위 클래스는 상위 클래스에 종속되어 있기 때문에 상위 클래스의 데이터 값에 직접 가능, 하위 클래스에서 MAX_NUM 상수 값 새로 생성 가능

상위 클래스에서 상수 값 정의
하위 클래스에서 상수 값 재정의 및 getter함수 정의

하위 클래스에서 위 사진과 같이 상수 값을 재정의 한 후 getter 함수를 정의해 리턴 값을 새로 정의된 상수 값으로 해준다.

상수 변경 테스트

테스트해보면 하위 클래스의 상수 값이 잘 나오는 걸 확인할 수 있다.

728x90
반응형

자바(Java) 사용자 입/출력과 버퍼(butter)

Posted by Space_Jin
2021. 12. 29. 20:03 Programming/Java, Kotlin
728x90
반응형

프로그램을 사용하다 보면 사용자 입력을 받는 경우가 많다.

 

c언어나 java의 경우 사용자 입력을 받을 때, 버퍼(buffer)라는 녀석에 값을 임시로 저장해두었다가 한번에 변수에 저장하는데  숫자와 문자가 다르게 동작해서 이상한 의도한 바와 다르게 되기도 한다.

 

▶  버퍼(buffer)의 이해

일단, 왜 버퍼라는 것이 존재하는지부터 알아야할 것 같다.

 

컴퓨터에 따라 다르겠지만 일반적인 컴퓨터(가정용)는 1초에 약 8천 ~ 1억 번 정도의 연산을 처리할 수 있다고 한다.

 

만약, 컴퓨터가 사용자에게 정보를 입력 받아야할 때, 우리가 정보를 모두 입력하기를 기다려야 한다면, 우리가 1 / 1억 초 안에 하나의 값을 입력하지 않는 한 컴퓨터는 그동안 계속 대기하고 있어야 한다. 

 

CPU입장에서는 속터지는 일이 아닐 수 없을 것이다. 그래서 등장한 것이 "버퍼"라는 개념이라고 한다.

 

어떤 변수에 사용자 입력 값을 저장할 때, 하나하나 차례로 저장하는 것이 아닌 임시로 버퍼에 저장해 두었다가 모든 입력이 완료되면(사용자가 엔터키를 누름) 한 번에 변수에 저장시키는 것이다. 입력을 하는 그 사이사이 시간에 컴퓨터는 다른 일을 하고 있을 수 있다.

 

키보드로 'WOW'라는 문자를 사용자에게 입력 받는다고 가정하자.

사용자가 키보드를 입력할때 마다 버퍼에는 문자가 하나씩 쌓이기 시작한다.

 

↓버퍼의 상태 변화

키보드 'W' 입력 W      
다음 입력 값이 있을 때까지 컴퓨터는 다른 작업을 처리...
키보드 'O' 입력 W O    
다음 입력 값이 있을 때까지 컴퓨터는 다른 작업을 처리...
키보드 'W' 입력 W O W  
다음 입력 값이 있을 때까지 컴퓨터는 다른 작업을 처리...
키보드 'Enter' 입력 W O W \n
사용자 입력 종료

사용자가 엔터키를 누르는 순간, 입력이 끝난 것으로 간주하고 버퍼에는 엔터를 포함해서 차례로 WOW\n가 남게 된다.

(\n는 컴퓨터에서 한 줄 띄기, enter를 의미한다.)

 

그 후 우리가 변수를 저장한다면, 버퍼에 남아있는 값들을 가져와 저장한다.

ex). 

>>>Scanner sc = new Scanner(System.in); 

>>>String wow = sc.nextLine();   // 사용자 입력으로 WOW 저장

 

wow라는 문자열 변수에 'WOW' 저장

 

▶  nextInt()와 nextLine()

버퍼의 특징 때문에 nextInt() 메서드를 사용해서 정수 값을 입력할 때, 주의해야 할 점이 생긴다.

 

nextInt() 메서드는 버퍼에 남아있는 연속된 숫자만 리턴한다.

 

간단한 예를 들어보면

 

>>> int num = sc.nextInt();

 

가 실행 되었을 때, 사용자 입력을 123으로 가정해보자 이때, 버퍼의 상태는 아래와 같다.

 

↓버퍼의 상태 변화

키보드 1 입력 1      
다음 입력 값이 있을 때까지 컴퓨터는 다른 작업을 처리...
키보드 2 입력 1 2    
다음 입력 값이 있을 때까지 컴퓨터는 다른 작업을 처리...
키보드 3 입력 1 2 3  
다음 입력 값이 있을 때까지 컴퓨터는 다른 작업을 처리...
키보드 'Enter' 입력 1 2 3 \n
사용자 입력 종료

사용자 입력이 종료되면, 이제 버퍼에 값들을 차례로 가져와 선언된 변수 num에 저장한다.

 

이때, nextInt() 메서드 자체는 연속된 번호만 저장하기 때문에 enter 값인 \n을 만나면 더 이상 버퍼에서 값을 가져오지 않는다.

 

즉, 123만 버퍼에서 가져오고 \n은 남겨둔다.

 

----최종 상태

 

num == 123

 

↓현재 버퍼의 상태 : \n이 남아있다.

\n      

 

nextInt() 메소드만 사용하면 큰 문제가 없어 보이지만 문자열 입력을 받아 저장하는 nextLine()을 함께 사용할 때, 문제가 생긴다.

 

위에서 정수 123을 받은 후 문자열을 추가로 입력 받는 함수로 수정해 보면 아래와 같다.

 

>>> int num = sc.nextInt();

>>> String str = sc.nextLine();

>>> System.out.println(num);

>>> System.out.println(str);

 

예상 사용자 입력 :

123

안녕 반가워

 

예상 출력:

123

안녕 반가워

 

실제 출력:

123

 

실제로 정수 123을 사용자 입력을 받은 후 문자열 값을 입력받지 않고 프로그램이 종료되어버린다.

 

이는 nextLine() 메서드의 특징인데 nextLine()은 값을 가져올 때 \n을 만날 때까지의 모든 값을 가져와 변수에 저장하고 \n값 역시 버퍼에서 빼내기 때문이다.

 

예를 들어 버퍼에 "안녕 반가워"라는 값이 들어있을 경우

 

전)

/s \n

nextLine()은 \n을 만나기 전까지의 값을 변수에 저장하고 \n도 제거한다.

 

후)

         

버퍼가 비어진다.

 

 

다시 돌아와

>>> int num = sc.nextInt();    // 123 입력

>>> String str = sc.nextLine();

>>> System.out.println(num);

>>> System.out.println(str);

 

nextInt()로 사용자 입력 123을 num에 저장한 후 버퍼의 상태는 아래와 같다.

 

↓현재 버퍼의 상태 : \n이 남아있다.

\n      

바로 이어서 nextLine()이 실행되면 버퍼에 \n이 남아있기에 \n까지의 값을 버퍼에서 빼내 오고 \n전까지 이 값을 str에 저장한다.

 

즉, str에는 아무 값도 저장되지 않고 버퍼는 비어지게 된다.

 

버퍼는 비어져 있지 않는다면 추가로 사용자 입력을 받지 않기 때문에 이러한 일이 발생한다.

 

해결 방법은 여러 방법이 존재하는데 str에 사용자 입력을 받기 전에 nextLine() 메서드를 미리 사용하여 버퍼를 비우는 방식이 있다.

 

ex):

>>> int num = sc.nextInt();    // 123 입력

>>> sc.nextLine();  // 버퍼에 남아있는 \n을 가져와 버퍼를 비움

>>> String str = sc.nextLine();   // 원하는 문자열 입력

>>> System.out.println(num);

>>> System.out.println(str);

 

C언어의 getChar()와 유사한 방식이라고 할 수 있다.

 

확실히 C와 java는 조금 불편한 부분이 있기는 하지만 더 원론적인 느낌이라서 재미있다.

728x90
반응형

0000~1111 자바(JAVA) 데이터 타입 크기와 형변환에 대해서...

Posted by Space_Jin
2021. 12. 29. 19:55 Programming/Java, Kotlin
728x90
반응형

자바는 왜 커피일까?

최근에 자바(JAVA)를 배우고 있습니다.

 

기계과를 전공하고 반도체 산업에서 일했었기 때문인지 기본 CS지식에 대해서 배울 때, 하드웨어와 관련된 부분이 상당히 흥미로웠습니다.

 

하드웨어보다 소프트웨어가 하고싶어서 넘어왔는데 소프트웨어를 배우면서 하드웨어에 관심이 가는 심보란...

 

▶ 데이터 타입과 메모리

대부분 비슷하겠지만 자바에서 숫자 데이터 타입은 정수와 실수로 나뉘는데 데이터 타입(메모리에 저장되는 크기)으로 표현하자면 아래와 같습니다.

 

정수 타입 : byte(1 byte), char(2 byte), short(2 byte), int(4 byte), long(8 byte)

 

실수 타입 : float(4 byte), double(8 byte)

 

의미를 알아볼 때, 정수로 비교하면 이해하기가 쉽습니다.

 

먼저 메모리에 저장되는 크기의 단위인 1byte = 8bit (메모리의 최소 단위)

 

우리가 흔히 컴퓨터는 0과 1로 이루어진다라고 말하는데 정확히 말하면 컴퓨터의 동작은 상당수의 트랜지스터의 동작의 조합이기 때문입니다.

 

간단하게 하나의 스위치는 on(1) / off(0) 2가지 동작을 하는데 컴퓨터는 이 스위치들의 조합으로 동작을 하기 때문입니다.

 

흔히 우리가 아는 모스부호와 같습니다. 점과 선으로 이루어진 모스부호를 조합해서 다양한 단어를 만들어서 문장을 전송하는 것과 같습니다.

모스부호

 

다시 돌아와서 데이터 타입은 얼마만큼의 숫자를 표현할 수 있을까?

 

먼저 간단한 정수형 타입인 byte (1byte)를 확인해 보겠습니다.

 

byte 타입의 크기인 1byte는 bit로 표현하면 8bit가 됩니다.(1byte = 8 bit) 위에서 1bit는 0과 1을 표현하는 2가지 동작을할 수 있다고 했죠 그렇기에 1bit는 2^1개의 숫자를 표현할 수 있습니다.

 

2bit는 몇 개의 숫자를 표현할 수 있을까요? 0과 1이 두 번 2x2 = 2^2 개를 표현할 수 있습니다.

00 01 10 11

즉, byte 타입의 데이터는 8bit => 2^8 = 256개의 숫자를 표현할 수 있습니다.

0000 0000 0000 0001 0000 0010 ... 1111 11101 1111 1110 1111 1111

byte :  -128 ~ 127까지 표현(음수를 포함해서 256개)

(byte는 signed 타입 즉, 음수와 양수를 모두 갖는 형식)

 

내가 JAVA에서 byte 타입 변수에 값을 할당할 때 -128 ~ 127까지는 오류 없이 저장할 수 있다는 의미입니다.

 

그럼 우리가 보통 가장 많이 사용하는 int는 32bit => 2^32 = 약 ±2억 1천 정도로 상당히 큰 수까지 무리 없이 저장 가능하다는 의미입니다.

 

▶ 데이터 타입을 나누는 이유?

 int의 데이터가 많다면 굳이 데이터 타입을 나눌필요가  있을까? 그냥 int나 더 큰 것으로 사용하면 될 텐데?라는 의문이 들 수도 있는데 사실 반도체가 많이 발전한 지금은 많이 느슨해졌을지 모르지만, 산업기계에 포함되어있는 메모리는 그 크기가 아주 작아서 내장된 소프트웨어를 최대한 효율적으로 만들어야 하는 부분이 있습니다.

 

즉, 메모리는 아낄수록 좋다. 용량 줄일 수 있으면 좋으니까...

 

▶ 자바에서의 형변환

 사실 개인적으로 이 부분이 상당히 재미있었습니다.

 

변수를 선언했을 당시에 정의했던 데이터 타입을 중간에 "형 변환"이라는 툴을 이용해서 변화시킬 수 있었습니다.

ex): byte 데이터 타입으로 선언한 변수 a의 범주를 벗어나는 값을 강제 형변환

>>> byte a;

>>> a = (int) 500;

최대 127까지의 숫자만 담을 수 있었던 byte 변수가 강제 형 변환을 통해서 500 값을 가지게 되었습니다?

 

사실 출력했을 때 엉뚱한 값이 나오게 됩니다.

500은 byte 데이터 형식의 표현 범위를 벗어나는 값이기 때문에 더 큰 데이터 타입을 가져야 합니다.

 

작은 틀에 큰 값 할당할 때 오류가 발생하는 것입니다.

그럼 반대의 경우는?

 

byte -> int처럼 작은 값을 큰 데이터 타입 형식의 틀에 담는 것은 오류 없이 진행됩니다.

 

▶ 정수 -> 실수

 

실제 세상에서 사용되는 값은 정수보다 실수가 더 많은 비중을 차지하게 될 것입니다.

 

그래서 우리는 간단하게 정수를 입력받고 실수로 반환해야 할 때가 많을 텐데 이때도 형 변환을 사용이 될 것 같습니다.

 

>>> int height = 180;

>>> int weigth = 75;

>>> float rate = (float) height / weight;

 

여기서 int (8 byte) -> float (4 byte)의 형 변환이 이뤄집니다.

 

정수만 봤을 때, 더 큰 데이터 형식이 작은 데이터 형식으로 형 변환이 이뤄지는 것이 잘못된 것처럼 보일 수 있으나,

실제로 데이터 타입의 메모리를 차지하는 크기가 아닌 숫자를 표현하는 범위가 중요합니다.

 

float이 int보다 더 많은 숫자를 표현할 수 있는 넓은 범위를 가지고 있기 때문에 형변환이 가능한 것입니다.

 

실수에 범위에 대한 부분은 차후 추가하겠습니다.

 

 

 

평소에 변수를 할당할 때, 데이터와 메모리에 대한 생각을 하면서 조금 더 효율적인 방법을 찾아가야 될 듯합니다.


잘못된 부분이 있으면 피드백 주시면 감사합니다.

728x90
반응형