Cannot invoke "String.equals(Object)" because "edit" is null

 

   Null Pointer, 생각보다 쉽게 보는 오류다보니 해결도 쉬워서 그렇게 어려운 에러는 아니었는데 생각보다 꽤 애먹었던 부분이 있어 기록으로 남기고자 한다. 보통 널포인터 오류가 나면 참조변수에 아무것도 없겠거니 하고 대수롭지 않게 여겨서 이런 상황이 발생했다 생각한다.

 

	<ul class="nav nav-pills">
      	<!-- R : 전체상품 불러오기 -->
	  	<li class="nav-item"><a class="nav-link"  href="products">All Book</a></li>
	  	<!-- C : 상품입력 -->
		<li class="nav-item"><a class="nav-link" href="AddBook">Add Book</a></li>
		<!-- U : 상품수정 -->
		<li class="nav-item"><a class="nav-link" href="products?edit=update">Book update</a></li>
		<!-- D : 상품삭제 -->
		<li class="nav-item"><a class="nav-link" href="products?edit=delete">Book delete</a></li>
	</ul>

 

   먼저 요청 부분이다. products에 매핑할 때 수정과 삭제는 각각 파라미터를 지정해주었다.

 

	<% String edit = (String) request.getParameter("edit"); %>

	<div class="p-5 mb-4 bg-body-tertiary rounded-3">
		<div class="container-fluid py-5">
			<h1 class="display-5 fw-bold">
				<% if(edit.equals("update")){ %> 도서 편집
				
				<% } else if(edit.equals("delete")){ %> 도서 삭제
				
				<% } else { %> 도서 목록 <% } %>
			</h1>
			<p class="col-md-8 fs-4">
				<% if(edit.equals("update") || edit.equals("delete"))
				{ out.print("BookEdit"); }
				else { out.print("BookList"); } %>	
			</p>
		</div>
	</div>

 

   그리고 Null에러가 발생한 위치이다. 단순히 넘어온 파라미터를 String 변수에 담아 equals() 함수를 사용하여 조건문을 통해 요청에 맞는 데이터가 출력되도록 작성했는데 에러가 발생했다. 그것도 이상하게 편집과 삭제 페이지는 정상적으로 작동했지만 아무것도 없는 전체 목록 조회에서만 오류가 생겼는데 이유를 찾아보니 좀 더 명확하게 표시하라는 조언을 듣고 Null값을 직접 입력하기로 했다.

 

	<%  Stirng edit = null;
    
		if(request.getAttribute("edit")!=null){
			edit = (String)request.getAttribute("edit");
		}
	%>

	<div class="p-5 mb-4 bg-body-tertiary rounded-3">
		<div class="container-fluid py-5">
			<h1 class="display-5 fw-bold">
				<% if(edit.equals("update")){ %> 도서 편집
				
				<% } else if(edit.equals("delete")){ %> 도서 삭제
				
				<% } else if(edit==null) { %> 도서 목록 <% } %> 
			</h1>
			<p class="col-md-8 fs-4">
				<% if(edit.equals("update") || edit.equals("delete"))
				{ out.print("BookEdit"); }
				else { out.print("BookList"); } %>	
			</p>
		</div>
	</div>

 

   위와 같이 수정했는데 당연하게도 되지 않았다 ^0^…

 

   그래서 진짜 이유를 살펴보니, String은 저번에도 말했다싶이 일반 변수가 아니라 객체다. 즉 edit은 참조변수가 되며, 참조변수에 아무런 주소값을 가지고 있지 않은 상태에서 equals() 함수 를 실행하려 했기 때문에 에러가 뜨는것! 실제로 맨 처음처럼 작성했더니 JSP페이지 자체에서도 DeadCode라고 주의를 주었다. 하지만 이게 왜 안되는지 이해를 못했으니 실행했고 결과는 500페이지가 반겨주었다.

 

   쉽게 말해 null인 상태에서 equals() 함수 자체가 실행되지 않으니 조건식에서 에러가 발생한 경우였다. equals() 함수는 일단 String객체를 받지 않았기 때문에 그 함수를 실행하려하니 동작하지 않는 것.

   (단, edit != null 같이 입력하면 당연하게도 작동이 된다)

 

	<%  Stirng edit = "main";
    
		if(request.getAttribute("edit")!=null){
			edit = (String)request.getAttribute("edit");
		}
	%>

	<div class="p-5 mb-4 bg-body-tertiary rounded-3">
		<div class="container-fluid py-5">
			<h1 class="display-5 fw-bold">
				<% if(edit.equals("update")){ %> 도서 편집
				
				<% } else if(edit.equals("delete")){ %> 도서 삭제
				
				<% } else { %> 도서 목록 <% } %> 
			</h1>
			<p class="col-md-8 fs-4">
				<% if(edit.equals("update") || edit.equals("delete"))
				{ out.print("BookEdit"); }
				else { out.print("BookList"); } %>	
			</p>
		</div>
	</div>

 

   다시 원인을 파악했으니 기존 JSP화면에 edit참조변수에 "main"이라는 값을 저장하고 실행하니 무사히 화면이 로드되는 것을 확인할 수 있었다. String객체에 대해 이제 완벽하게 다 안다고 생각했는데 이런 오류를 통해 다시 기초부터 열심히 해야겠다는 생각이 들었다...

'오류노트' 카테고리의 다른 글

005  (0) 2024.11.12
Type mismatch & Cannot cast  (0) 2024.10.27

     데이터 베이스 작성     

create database bookmarketdb;
use bookmarketdb;

create table if not exists book(
	b_id varchar(10) not null primary key,
	b_name varchar(20),
	b_unitPrice integer,
	b_author varchar(20),
	b_description text,
	b_publisher varchar(20),
	b_category varchar(20),
	b_unitsInStock long,
	b_releaseDate varchar(20),
	b_condition varchar(20),
	b_filename varchar(20)
) default charset=utf8;

 

   BookMarket에 등록될 책들의 정보를 저장할 데이터 베이스를 만들고 book 테이블을 추가했다. 원래 데이터베이스와 DTO객체의 변수명은 통일시키는 것이 좋은데, 교재를 무심코 따라하다 보니 다른 변수명을 가지게 됐다. 나중에 연결할 때는 확인해서 설계단계를 꼼꼼히 거치도록 해야겠다.

 

     데이터베이스 연결하기     

private static Connection DBconn() {
		Connection conn = null;
		
		try {
			
			Class.forName("com.mysql.jdbc.Driver");
			
			String url = "jdbc:mysql://localhost:3306/bookmarketdb";
			String user = "root";
			String pw = "1234";
			
			conn = DriverManager.getConnection(url,user,pw);
			
			System.out.println("데이터 베이스 연결 성공");
		} catch(Exception e) {System.out.println("데이터 베이스 연결 실패");}
		
		return conn;
	}

 

   forName() 함수를 통해 드라이버를 로딩한 후, Connection 객체를 사용하여 데이터 베이스에 연결한다. CRUD를 돌리기 위해서 제일 먼저 되어야 하는 기능이므로 항상 코드를 작성하는 것 보다 함수로 만들어 필요할 때 호출하도록 만들었다.

 

     모델(BookRepository)     

	private BookRepository() {} //기본 생성자
	
	private static BookRepository repository = new BookRepository();
	//생성자 호출을 미리 해둠 (싱글톤)
	//private - 외부에서 접근을 막기 때문에, 함수도 같이 생성되나 접근할 수 없음
	
	public static BookRepository getRepository() {
		return repository;
		//생성된 객체를 반환
	}

 

    CRUD를 제대로 배우기 전, MVC 수업만 들을 때 까지만 해도 나는 데이터베이스는 모델이랑 같은 의미인 줄 알았다. 하지만 데이터베이스와 모델은 엄연히 다른 것으로, 데이터 베이스는 말 그대로 데이터를 보관하는 저장소지만 모델은 데이터 베이스로 가기 위한 임시 저장소에 가깝다고 보면 될 것 같다. 따라서 Java에서 보내줄 데이터를 임시로 저장하기 위한 공간을 매번 새로 생성할 이유는 없으니 싱글톤 패턴을 사용하여 모델을 하나로 유지하는 작업을 진행했다.

 

   기본 생성자 또한 다른 곳에서 생성될 이유가 없으므로 접근 제어자로 외부의 사용을 막고, 대신 모두가 접근 가능한 함수를 생성하여(public static) 고정된 저장소를 new 할 수 있도록 만들어준다.

 

     Read 컨트롤러     

	@WebServlet("/products")
	public class Read_Controller extends HttpServlet{

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
	throws ServletException, IOException {
		System.out.println("전체상품 컨트롤 페이지 (Read)");
		
		BookRepository dao = BookRepository.getRepository();
		ArrayList<Book> arr = dao.getAllBooks();
		
		req.setAttribute("list", arr);
		RequestDispatcher rd = req.getRequestDispatcher("books.jsp");
		rd.forward(req, resp);
	}

 

   이제 HTML에서 <a>태그나 <form>태그를 통해 요청이 발생하면 @webServlet() 으로 매핑된 컨트롤러 클래스에 도착하게 된다. 맨 처음 모든 상품을 보여주기 위한 컨트롤러를 위와 같이 작성했다. 먼저 전체상품을 사용자의 화면에 보여주려면 도서들의 정보가 필요하다. 이 정보는 모두 데이터베이스에 저장되어 있으므로 모델을 통해 데이터베이스에 접근하여 도서들의 객체 정보를 DTO로 담은 ArrayList 배열 형태로 받아온다. 이를 req(request)에 set() 함수를 통해 저장하고, forward함수로 해당 jsp 페이지로 이동하면 데이터베이스에서 그대로 받은 정보를 HTML에 전송할 수 있게 된다.

 

     Read :: getAllBooks     

	public ArrayList<Book> getAllBooks() {

		//해당 메서드는 도서 목록을 반환
		
		PreparedStatement ps = null;
		ResultSet rs = null;
		ArrayList<Book> list = new ArrayList<Book>();
		
		try {
			Connection conn = DBconn();
			
			String sql = "select * from book";
			ps = conn.prepareStatement(sql);
			rs = ps.executeQuery();	
			System.out.println("전체 도서 목록을 가져옵니다.");
			
			while(rs.next()) {
				Book book = new Book();
				
				book.setBookId(rs.getString("b_id"));
				book.setName(rs.getString("b_name"));
				book.setUnitPrice(rs.getInt("b_unitPrice"));
				book.setAuthor(rs.getString("b_author"));
				book.setDescription(rs.getString("b_description"));
				book.setPublisher(rs.getString("b_publisher"));
				book.setCategory(rs.getString("b_category"));
				book.setUnitsInStock(rs.getLong("b_unitsInStock"));
				book.setReleaseDate(rs.getString("b_releaseDate"));
				book.setCondition(rs.getString("b_condition"));
				book.setFilename(rs.getString("b_filename"));
				
				System.out.println("확인용 아이디"+rs.getString("b_id"));
				
				list.add(book);
			}
			
			rs.close();
			ps.close();
			conn.close();
			
		}
		catch(Exception e) { System.out.println("전체 도서 불러오기 실패"); }
		
		System.out.println("반환 완료");
		return list;
	}

 

   전체 도서 목록을 반환하는 함수이다. 모든 정보를 반환하기 때문에 따로 파라미터를 받을 건 없으며, 목록을 ArrayList로 반환해야 하기 때문에 반환타입이 존재한다. 이제 데이터베이스에 연결하는 함수는 이미 작성해두었으니 Connection 객체를 가져온 다음 연결되어 있는 데이터베이스에 SQL로 명령문을 내려야 한다.

 

   이때 명령문을 전달하기 위해서는 Statement객체를 사용해야 하는데, Statement객체보다는 PreparedStatement를 사용하는 것을 권장한다(객체생성의 문제가 있다고 한다). 전체 도서목록을 가져오기 위해서는 테이블에 저장되어 있는 모든 데이터를 가져와야하기 때문에 "SELECT * FROM (테이블 명)" 을 통해 모든 내용을 불러온 후, Connection 객체의 도움을 받아 Java에서 데이터베이스로 명령어를 전송해준다. 이 때 Statement객체와 PreparedsStatement객체의 사용법이 아주 살짝 차이가 있으니 알아두도록 하자. Statement객체는 명령어를 전송할 커넥션 객체를 생성한 후, execute문을 통해 명령어를 전송하지만 PreparedStatement는 커넥션 객체를 생성할 때 명령어를 전송하고 이후 execute문으로 실행을 한다.

 

   이렇게 객체를 통해 명령어가 전송되면 SQL에서 반환해주는 데이터를 ResultSet 객체를 통해 다시 받아온다. ResultSet객체의 내용은 데이터베이스에서 보여주는 엑셀 화면과 비슷하다 생각하면 쉽다. 그래서 모든 데이터를 한 줄씩(행) 읽어와야 하는데, 이 때 사용하는 함수가 next() 함수이다. 1행, 2행, 3행... 하나씩 넘기며 데이터가 있는지 확인하고 데이터가 존재하면 TRUE, null데이터를 만나면 FALSE를 반환하여 while문을 빠져나오게 된다.

   ResultSet에서 size()나 length() 함수를 사용하지 못하는 이유는 크기를 반환해주지 않기 때문이다. 따라서 데이터가 나오지 않을 때까지 다음줄로 넘기는 next()함수를 사용하는 것이다.

 

   이렇게 반복문을 통해 정보를 받아올 때마다 컨트롤러에 반환해야하는 ArrayList객체에 add()함수를 사용하여 불러온 객체를 저장한다. 이 때, 반복문은 다시 처음부터 실행하기 때문에 DTO객체는 계속해서 새로운 주소를 생성하여 데이터를 저장하기 위해서 반복문 안에서 객체 생성을 했고, ArrayList는 이 내용물이 그대로 유지되어야 하기 때문에 반복문 밖에서 생성해야 한다.

 

     Read JSP     

<% ArrayList<Book> listOfBooks = (ArrayList<Book>)request.getAttribute("list"); %>

<div class="row align-items-md-stretch text-center">
	<%
		for(int i=0; i<listOfBooks.size(); i++){
			Book book = listOfBooks.get(i);
	%>
			
	<div class="col-md-4">
		<div class="h-100 p-2">
			<img src="resources/img/<%= book.getFilename() %>">
			<h5><b><%= book.getName() %></b></h5>
			<p><%= book.getAuthor() %></p>
			<p><%= book.getPublisher() %> | <%= book.getReleaseDate() %></p>
			<p><%= book.getDescription() %>...</p>
			<p><%= book.getUnitPrice() %>원</p>
			<p>
				<% if(edit.equals("update")) { %>
                <a href="./update?id=<%=book.getBookId()%>"> 수정하기 &raquo;
				<% } else if(edit.equals("delete")) { %>
                <a href="#" onclick="deleteConfirm('<%=book.getBookId()%>')">도서삭제 &raquo;
				<% } else { %>
                <a href="./book?id=<%=book.getBookId()%>"> 상세 정보 &raquo; <% } %>		
			</a></p>
		</div>
	</div>
	<%
		}
	%>
</div>

<script type="text/javascript">
	function deleteConfirm(id) {
		if(confirm("!!해당 도서를 삭제합니다!!")==true){
			location.href = "deleteBook?id=" + id;
		} else { return; }
	}
</script>

 

   이제 받아온 정보를 HTML로 바꾸어 클라이언트 화면에 내보내는 작업을 진행했다. 컨트롤러에서 받아온 리스트를 다시 get()함수를 통해 받아주고(Object로 저장됐기 때문에 형변환 필요), ArrayList는 크기를 알 수 있으므로 for문 사용으로 꺼내는 것이 가능하다. 필요한 정보를 양식에 맞게 HTML 형식으로 작성하면 전체 도서 목록을 반환하는 페이지가 완성된다.

 

+ Recent posts